| | |
| | | memo, |
| | | useCallback, |
| | | useEffect, |
| | | useMemo, |
| | | useRef, |
| | | useState, |
| | | } from 'react' |
| | | import useSWR from 'swr' |
| | | import { setAutoFreeze } from 'immer' |
| | | import { |
| | | useEventListener, |
| | |
| | | import './style.css' |
| | | import type { |
| | | Edge, |
| | | EnvironmentVariable, |
| | | Node, |
| | | } from './types' |
| | | import { |
| | | ControlMode, |
| | | SupportUploadFileTypes, |
| | | } from './types' |
| | | import { WorkflowContextProvider } from './context' |
| | | import { |
| | | useDSL, |
| | | useEdgesInteractions, |
| | | useFetchToolsData, |
| | | useNodesInteractions, |
| | | useNodesReadOnly, |
| | | useNodesSyncDraft, |
| | |
| | | useSelectionInteractions, |
| | | useShortcuts, |
| | | useWorkflow, |
| | | useWorkflowInit, |
| | | useWorkflowReadOnly, |
| | | useWorkflowRefreshDraft, |
| | | useWorkflowUpdate, |
| | | } from './hooks' |
| | | import Header from './header' |
| | | import CustomNode from './nodes' |
| | | import CustomNoteNode from './note-node' |
| | | import { CUSTOM_NOTE_NODE } from './note-node/constants' |
| | | import CustomIterationStartNode from './nodes/iteration-start' |
| | | import { CUSTOM_ITERATION_START_NODE } from './nodes/iteration-start/constants' |
| | | import CustomLoopStartNode from './nodes/loop-start' |
| | | import { CUSTOM_LOOP_START_NODE } from './nodes/loop-start/constants' |
| | | import CustomSimpleNode from './simple-node' |
| | | import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' |
| | | import Operator from './operator' |
| | | import CustomEdge from './custom-edge' |
| | | import CustomConnectionLine from './custom-connection-line' |
| | | import Panel from './panel' |
| | | import Features from './features' |
| | | import HelpLine from './help-line' |
| | | import CandidateNode from './candidate-node' |
| | | import PanelContextmenu from './panel-contextmenu' |
| | | import NodeContextmenu from './node-contextmenu' |
| | | import SyncingDataModal from './syncing-data-modal' |
| | | import UpdateDSLModal from './update-dsl-modal' |
| | | import DSLExportConfirmModal from './dsl-export-confirm-modal' |
| | | import LimitTips from './limit-tips' |
| | | import { |
| | | useStore, |
| | | useWorkflowStore, |
| | | } from './store' |
| | | import { |
| | | CUSTOM_EDGE, |
| | | initialEdges, |
| | | initialNodes, |
| | | } from './utils' |
| | | import { |
| | | CUSTOM_NODE, |
| | | DSL_EXPORT_CHECK, |
| | | ITERATION_CHILDREN_Z_INDEX, |
| | | WORKFLOW_DATA_UPDATE, |
| | | } from './constants' |
| | | import { WorkflowHistoryProvider } from './workflow-history-store' |
| | | import Loading from '@/app/components/base/loading' |
| | | import { FeaturesProvider } from '@/app/components/base/features' |
| | | import type { Features as FeaturesData } from '@/app/components/base/features/types' |
| | | import { useFeaturesStore } from '@/app/components/base/features/hooks' |
| | | import { useEventEmitterContextContext } from '@/context/event-emitter' |
| | | import Confirm from '@/app/components/base/confirm' |
| | | import DatasetsDetailProvider from './datasets-detail-store/provider' |
| | | import { HooksStoreContextProvider } from './hooks-store' |
| | | import type { Shape as HooksStoreShape } from './hooks-store' |
| | | import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' |
| | | import { fetchFileUploadConfig } from '@/service/common' |
| | | |
| | | const nodeTypes = { |
| | | [CUSTOM_NODE]: CustomNode, |
| | | [CUSTOM_NOTE_NODE]: CustomNoteNode, |
| | | [CUSTOM_SIMPLE_NODE]: CustomSimpleNode, |
| | | [CUSTOM_ITERATION_START_NODE]: CustomIterationStartNode, |
| | | [CUSTOM_LOOP_START_NODE]: CustomLoopStartNode, |
| | | } |
| | | const edgeTypes = { |
| | | [CUSTOM_EDGE]: CustomEdge, |
| | | [CUSTOM_NODE]: CustomEdge, |
| | | } |
| | | |
| | | export type WorkflowProps = { |
| | | type WorkflowProps = { |
| | | nodes: Node[] |
| | | edges: Edge[] |
| | | viewport?: Viewport |
| | | children?: React.ReactNode |
| | | onWorkflowDataUpdate?: (v: any) => void |
| | | } |
| | | export const Workflow: FC<WorkflowProps> = memo(({ |
| | | const Workflow: FC<WorkflowProps> = memo(({ |
| | | nodes: originalNodes, |
| | | edges: originalEdges, |
| | | viewport, |
| | | children, |
| | | onWorkflowDataUpdate, |
| | | }) => { |
| | | const workflowContainerRef = useRef<HTMLDivElement>(null) |
| | | const workflowStore = useWorkflowStore() |
| | | const reactflow = useReactFlow() |
| | | const featuresStore = useFeaturesStore() |
| | | const [nodes, setNodes] = useNodesState(originalNodes) |
| | | const [edges, setEdges] = useEdgesState(originalEdges) |
| | | const showFeaturesPanel = useStore(state => state.showFeaturesPanel) |
| | | const controlMode = useStore(s => s.controlMode) |
| | | const nodeAnimation = useStore(s => s.nodeAnimation) |
| | | const showConfirm = useStore(s => s.showConfirm) |
| | | const showImportDSLModal = useStore(s => s.showImportDSLModal) |
| | | |
| | | const { |
| | | setShowConfirm, |
| | | setControlPromptEditorRerenderKey, |
| | | setShowImportDSLModal, |
| | | setSyncWorkflowDraftHash, |
| | | } = workflowStore.getState() |
| | | const { |
| | |
| | | } = useNodesSyncDraft() |
| | | const { workflowReadOnly } = useWorkflowReadOnly() |
| | | const { nodesReadOnly } = useNodesReadOnly() |
| | | |
| | | const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) |
| | | |
| | | const { eventEmitter } = useEventEmitterContextContext() |
| | | |
| | | eventEmitter?.useSubscription((v: any) => { |
| | |
| | | if (v.payload.viewport) |
| | | reactflow.setViewport(v.payload.viewport) |
| | | |
| | | if (v.payload.features && featuresStore) { |
| | | const { setFeatures } = featuresStore.getState() |
| | | |
| | | setFeatures(v.payload.features) |
| | | } |
| | | |
| | | if (v.payload.hash) |
| | | setSyncWorkflowDraftHash(v.payload.hash) |
| | | |
| | | onWorkflowDataUpdate?.(v.payload) |
| | | |
| | | setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) |
| | | } |
| | | if (v.type === DSL_EXPORT_CHECK) |
| | | setSecretEnvList(v.payload.data as EnvironmentVariable[]) |
| | | }) |
| | | |
| | | useEffect(() => { |
| | |
| | | // eslint-disable-next-line react-hooks/exhaustive-deps |
| | | }, []) |
| | | |
| | | const { handleRefreshWorkflowDraft } = useWorkflowRefreshDraft() |
| | | const { handleRefreshWorkflowDraft } = useWorkflowUpdate() |
| | | const handleSyncWorkflowDraftWhenPageClose = useCallback(() => { |
| | | if (document.visibilityState === 'hidden') |
| | | syncWorkflowDraftWhenPageClose() |
| | |
| | | }) |
| | | } |
| | | }) |
| | | const { handleFetchAllTools } = useFetchToolsData() |
| | | useEffect(() => { |
| | | handleFetchAllTools('builtin') |
| | | handleFetchAllTools('custom') |
| | | handleFetchAllTools('workflow') |
| | | }, [handleFetchAllTools]) |
| | | |
| | | const { |
| | | handleNodeDragStart, |
| | |
| | | } = useSelectionInteractions() |
| | | const { |
| | | handlePaneContextMenu, |
| | | handlePaneContextmenuCancel, |
| | | } = usePanelInteractions() |
| | | const { |
| | | isValidConnection, |
| | | } = useWorkflow() |
| | | const { |
| | | exportCheck, |
| | | handleExportDSL, |
| | | } = useDSL() |
| | | |
| | | useOnViewportChange({ |
| | | onEnd: () => { |
| | |
| | | <div |
| | | id='workflow-container' |
| | | className={` |
| | | relative h-full w-full min-w-[960px] |
| | | relative w-full min-w-[960px] h-full |
| | | ${workflowReadOnly && 'workflow-panel-animation'} |
| | | ${nodeAnimation && 'workflow-node-animation'} |
| | | `} |
| | |
| | | > |
| | | <SyncingDataModal /> |
| | | <CandidateNode /> |
| | | <Header /> |
| | | <Panel /> |
| | | <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> |
| | | { |
| | | showFeaturesPanel && <Features /> |
| | | } |
| | | <PanelContextmenu /> |
| | | <NodeContextmenu /> |
| | | <HelpLine /> |
| | |
| | | /> |
| | | ) |
| | | } |
| | | { |
| | | showImportDSLModal && ( |
| | | <UpdateDSLModal |
| | | onCancel={() => setShowImportDSLModal(false)} |
| | | onBackup={exportCheck} |
| | | onImport={handlePaneContextmenuCancel} |
| | | /> |
| | | ) |
| | | } |
| | | { |
| | | secretEnvList.length > 0 && ( |
| | | <DSLExportConfirmModal |
| | | envList={secretEnvList} |
| | | onConfirm={handleExportDSL} |
| | | onClose={() => setSecretEnvList([])} |
| | | /> |
| | | ) |
| | | } |
| | | <LimitTips /> |
| | | {children} |
| | | <ReactFlow |
| | | nodeTypes={nodeTypes} |
| | | edgeTypes={edgeTypes} |
| | |
| | | onSelectionDrag={handleSelectionDrag} |
| | | onPaneContextMenu={handlePaneContextMenu} |
| | | connectionLineComponent={CustomConnectionLine} |
| | | // TODO: For LOOP node, how to distinguish between ITERATION and LOOP here? Maybe both are the same? |
| | | connectionLineContainerStyle={{ zIndex: ITERATION_CHILDREN_Z_INDEX }} |
| | | defaultViewport={viewport} |
| | | multiSelectionKeyCode={null} |
| | |
| | | nodesConnectable={!nodesReadOnly} |
| | | nodesFocusable={!nodesReadOnly} |
| | | edgesFocusable={!nodesReadOnly} |
| | | panOnScroll={false} |
| | | panOnDrag={controlMode === ControlMode.Hand && !workflowReadOnly} |
| | | zoomOnPinch={!workflowReadOnly} |
| | | zoomOnScroll={!workflowReadOnly} |
| | |
| | | </div> |
| | | ) |
| | | }) |
| | | Workflow.displayName = 'Workflow' |
| | | |
| | | type WorkflowWithInnerContextProps = WorkflowProps & { |
| | | hooksStore?: Partial<HooksStoreShape> |
| | | } |
| | | export const WorkflowWithInnerContext = memo(({ |
| | | hooksStore, |
| | | ...restProps |
| | | }: WorkflowWithInnerContextProps) => { |
| | | const WorkflowWrap = memo(() => { |
| | | const { |
| | | data, |
| | | isLoading, |
| | | } = useWorkflowInit() |
| | | const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig) |
| | | |
| | | const nodesData = useMemo(() => { |
| | | if (data) |
| | | return initialNodes(data.graph.nodes, data.graph.edges) |
| | | |
| | | return [] |
| | | }, [data]) |
| | | const edgesData = useMemo(() => { |
| | | if (data) |
| | | return initialEdges(data.graph.edges, data.graph.nodes) |
| | | |
| | | return [] |
| | | }, [data]) |
| | | |
| | | if (!data || isLoading) { |
| | | return ( |
| | | <HooksStoreContextProvider {...hooksStore}> |
| | | <Workflow {...restProps} /> |
| | | </HooksStoreContextProvider> |
| | | <div className='flex justify-center items-center relative w-full h-full'> |
| | | <Loading /> |
| | | </div> |
| | | ) |
| | | }) |
| | | |
| | | type WorkflowWithDefaultContextProps = |
| | | Pick<WorkflowProps, 'edges' | 'nodes'> |
| | | & { |
| | | children: React.ReactNode |
| | | } |
| | | |
| | | const WorkflowWithDefaultContext = ({ |
| | | nodes, |
| | | edges, |
| | | children, |
| | | }: WorkflowWithDefaultContextProps) => { |
| | | const features = data.features || {} |
| | | const initialFeatures: FeaturesData = { |
| | | file: { |
| | | image: { |
| | | enabled: !!features.file_upload?.image?.enabled, |
| | | number_limits: features.file_upload?.image?.number_limits || 3, |
| | | transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], |
| | | }, |
| | | enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled), |
| | | allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image], |
| | | allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`), |
| | | allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'], |
| | | number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3, |
| | | fileUploadConfig: fileUploadConfigResponse, |
| | | }, |
| | | opening: { |
| | | enabled: !!features.opening_statement, |
| | | opening_statement: features.opening_statement, |
| | | suggested_questions: features.suggested_questions, |
| | | }, |
| | | suggested: features.suggested_questions_after_answer || { enabled: false }, |
| | | speech2text: features.speech_to_text || { enabled: false }, |
| | | text2speech: features.text_to_speech || { enabled: false }, |
| | | citation: features.retriever_resource || { enabled: false }, |
| | | moderation: features.sensitive_word_avoidance || { enabled: false }, |
| | | } |
| | | |
| | | return ( |
| | | <ReactFlowProvider> |
| | | <WorkflowHistoryProvider |
| | | nodes={nodes} |
| | | edges={edges} > |
| | | <DatasetsDetailProvider nodes={nodes}> |
| | | {children} |
| | | </DatasetsDetailProvider> |
| | | nodes={nodesData} |
| | | edges={edgesData} > |
| | | <FeaturesProvider features={initialFeatures}> |
| | | <Workflow |
| | | nodes={nodesData} |
| | | edges={edgesData} |
| | | viewport={data?.graph.viewport} |
| | | /> |
| | | </FeaturesProvider> |
| | | </WorkflowHistoryProvider> |
| | | </ReactFlowProvider> |
| | | ) |
| | | }) |
| | | WorkflowWrap.displayName = 'WorkflowWrap' |
| | | |
| | | const WorkflowContainer = () => { |
| | | return ( |
| | | <WorkflowContextProvider> |
| | | <WorkflowWrap /> |
| | | </WorkflowContextProvider> |
| | | ) |
| | | } |
| | | |
| | | export default memo(WorkflowWithDefaultContext) |
| | | export default memo(WorkflowContainer) |