'use client' import type { ReactNode } from 'react' import type { Features as FeaturesData } from '@/app/components/base/features/types' import type { InjectWorkflowStoreSliceFn } from '@/app/components/workflow/store' import { useQueryState } from 'nuqs' import { useCallback, useEffect, useMemo, useRef, useState, } from 'react' import { useStore as useAppStore } from '@/app/components/app/store' import { FeaturesProvider } from '@/app/components/base/features' import Loading from '@/app/components/base/loading' import WorkflowWithDefaultContext from '@/app/components/workflow' import { useCollaboration } from '@/app/components/workflow/collaboration' import { collaborationManager } from '@/app/components/workflow/collaboration/core/collaboration-manager' import { WorkflowContextProvider, } from '@/app/components/workflow/context' import { HeaderShell } from '@/app/components/workflow/header' import OnlineUsers from '@/app/components/workflow/header/online-users' import { useStore, useWorkflowStore } from '@/app/components/workflow/store' import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status' import { BlockEnum, ViewType, } from '@/app/components/workflow/types' import { initialEdges, initialNodes, } from '@/app/components/workflow/utils' import { useAppContext } from '@/context/app-context' import { useEventEmitterContextContext } from '@/context/event-emitter' import dynamic from '@/next/dynamic' import { useSearchParams } from '@/next/navigation' import { upgradeAppRuntime } from '@/service/apps' import { fetchRunDetail } from '@/service/log' import { useAppTriggers } from '@/service/use-tools' import { AppModeEnum } from '@/types/app' import { useFeatures } from '../base/features/hooks' import ViewPicker from '../workflow/view-picker' import SandboxMigrationModal from './components/sandbox-migration-modal' import UpgradedFromBanner from './components/upgraded-from-banner' import WorkflowAppMain from './components/workflow-main' import { useGetRunAndTraceUrl } from './hooks/use-get-run-and-trace-url' import { useNodesSyncDraft } from './hooks/use-nodes-sync-draft' import { useWorkflowInit, } from './hooks/use-workflow-init' import { parseAsViewType, WORKFLOW_VIEW_PARAM_KEY } from './search-params' import { createWorkflowSlice } from './store/workflow/workflow-slice' import { buildInitialFeatures, buildTriggerStatusMap, coerceReplayUserInputs, } from './utils' import { getSandboxMigrationDismissed, setSandboxMigrationDismissed } from './utils/sandbox-migration-storage' const SkillMain = dynamic(() => import('@/app/components/workflow/skill/main'), { ssr: false, }) const CollaborationSession = () => { const appId = useStore(s => s.appId) useCollaboration(appId || '') return null } type WorkflowViewContentProps = { renderGraph: (headerLeftSlot: ReactNode) => ReactNode reload: () => Promise } const WorkflowViewContent = ({ renderGraph, reload, }: WorkflowViewContentProps) => { const features = useFeatures(s => s.features) const isSupportSandbox = !!features.sandbox?.enabled const isResponding = useStore(s => s.isResponding) const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType) const { syncWorkflowDraftImmediately } = useNodesSyncDraft() const pendingSyncRef = useRef | null>(null) const [isGraphRefreshing, setIsGraphRefreshing] = useState(false) const refreshGraph = useCallback(() => { setIsGraphRefreshing(true) return reload().finally(() => { setIsGraphRefreshing(false) }) }, [reload]) const handleViewTypeChange = useCallback((type: ViewType) => { if (viewType === ViewType.graph && type !== viewType) pendingSyncRef.current = syncWorkflowDraftImmediately(true).catch(() => { }) doSetViewType(type) if (type === ViewType.graph) { const pending = pendingSyncRef.current if (pending) { pending.finally(() => { refreshGraph() }) pendingSyncRef.current = null } else { refreshGraph() } } }, [doSetViewType, refreshGraph, syncWorkflowDraftImmediately, viewType]) useEffect(() => { if (!isSupportSandbox) { collaborationManager.emitGraphViewActive(true) return () => { collaborationManager.emitGraphViewActive(false) } } collaborationManager.emitGraphViewActive(viewType === ViewType.graph) return () => { collaborationManager.emitGraphViewActive(false) } }, [isSupportSandbox, viewType]) if (!isSupportSandbox) return renderGraph(null) const viewPicker = ( ) const viewPickerDock = (
{viewPicker}
) return (
{viewType === ViewType.graph ? ( isGraphRefreshing ? ( <> {viewPickerDock}
) : renderGraph(viewPicker) ) : ( <> {viewPickerDock} )}
) } const WorkflowAppWithAdditionalContext = () => { const { data, isLoading, fileUploadConfigResponse, reload, } = useWorkflowInit() const workflowStore = useWorkflowStore() const { isLoadingCurrentWorkspace, currentWorkspace } = useAppContext() const [showMigrationModal, setShowMigrationModal] = useState(false) const [isUpgrading, setIsUpgrading] = useState(false) const lastCheckedAppIdRef = useRef(null) // Initialize trigger status at application level const { setTriggerStatuses } = useTriggerStatusStore() const appDetail = useAppStore(s => s.appDetail) const appId = appDetail?.id const handleCloseMigrationModal = useCallback(() => { setSandboxMigrationDismissed(appId) setShowMigrationModal(false) }, [appId]) const { eventEmitter } = useEventEmitterContextContext() eventEmitter?.useSubscription((v) => { if (typeof v === 'object' && v?.type === 'upgrade-runtime-click') setShowMigrationModal(true) }) const showUpgradeRuntimeModal = useStore(s => s.showUpgradeRuntimeModal) const setShowUpgradeRuntimeModal = useStore(s => s.setShowUpgradeRuntimeModal) useEffect(() => { if (showUpgradeRuntimeModal) { // eslint-disable-next-line react/set-state-in-effect setShowMigrationModal(true) setShowUpgradeRuntimeModal(false) } }, [showUpgradeRuntimeModal, setShowUpgradeRuntimeModal]) const handleUpgradeRuntime = useCallback(async () => { if (!appId || isUpgrading) return setIsUpgrading(true) try { const res = await upgradeAppRuntime(appId) if (res.result === 'success' && res.new_app_id) { const appName = appDetail?.name || '' const params = new URLSearchParams({ upgraded_from: appId, upgraded_from_name: appName, }) window.location.href = `/app/${res.new_app_id}/workflow?${params.toString()}` } } finally { setIsUpgrading(false) } }, [appId, appDetail?.name, isUpgrading]) const isWorkflowMode = appDetail?.mode === AppModeEnum.WORKFLOW const { data: triggersResponse } = useAppTriggers(isWorkflowMode ? appId : undefined, { staleTime: 5 * 60 * 1000, // 5 minutes cache refetchOnWindowFocus: false, }) // Sync trigger statuses to store when data loads useEffect(() => { if (triggersResponse?.data) { setTriggerStatuses(buildTriggerStatusMap(triggersResponse.data)) } }, [triggersResponse?.data, setTriggerStatuses]) // Cleanup on unmount useEffect(() => { return () => { // Reset the loaded flag when component unmounts workflowStore.setState({ isWorkflowDataLoaded: false }) // Cancel any pending debounced sync operations const { debouncedSyncWorkflowDraft } = workflowStore.getState() // The debounced function from lodash has a cancel method if (debouncedSyncWorkflowDraft && 'cancel' in debouncedSyncWorkflowDraft) (debouncedSyncWorkflowDraft as { cancel: () => void }).cancel() } }, [workflowStore]) const isSandboxRuntime = appDetail?.runtime_type === 'sandboxed' const isSandboxFeatureEnabled = data?.features?.sandbox?.enabled === true const isSandboxed = isSandboxRuntime || isSandboxFeatureEnabled const nodesData = useMemo(() => { if (data) { const processedNodes = initialNodes(data.graph.nodes, data.graph.edges) const resolvedNodes = isSandboxed ? processedNodes.map((node) => { if (node.data.type !== BlockEnum.LLM) return node return { ...node, data: { ...node.data, _iconTypeOverride: BlockEnum.Agent, }, } }) : processedNodes collaborationManager.setNodes([], resolvedNodes) return resolvedNodes } return [] }, [data, isSandboxed]) const edgesData = useMemo(() => { if (data) { const processedEdges = initialEdges(data.graph.edges, data.graph.nodes) collaborationManager.setEdges([], processedEdges) return processedEdges } return [] }, [data]) const searchParams = useSearchParams() const { getWorkflowRunAndTraceUrl } = useGetRunAndTraceUrl() const replayRunId = searchParams.get('replayRunId') const upgradedFromId = searchParams.get('upgraded_from') const upgradedFromName = searchParams.get('upgraded_from_name') useEffect(() => { if (!replayRunId) return const { runUrl } = getWorkflowRunAndTraceUrl(replayRunId) if (!runUrl) return fetchRunDetail(runUrl).then((res) => { const { setInputs, setShowInputsPanel, setShowDebugAndPreviewPanel } = workflowStore.getState() const rawInputs = res.inputs let parsedInputs: unknown = rawInputs if (typeof rawInputs === 'string') { try { parsedInputs = JSON.parse(rawInputs) as unknown } catch (error) { console.error('Failed to parse workflow run inputs', error) return } } const userInputs = coerceReplayUserInputs(parsedInputs) if (!userInputs || !Object.keys(userInputs).length) return setInputs(userInputs) setShowInputsPanel(true) setShowDebugAndPreviewPanel(true) }) }, [replayRunId, workflowStore, getWorkflowRunAndTraceUrl]) const isDataReady = !(!data || isLoading || isLoadingCurrentWorkspace || !currentWorkspace.id) const sandboxEnabled = isSandboxFeatureEnabled const setNeedsRuntimeUpgrade = useAppStore(s => s.setNeedsRuntimeUpgrade) useEffect(() => { if (!isDataReady || !appId) return setNeedsRuntimeUpgrade(!sandboxEnabled) if (lastCheckedAppIdRef.current !== appId) { lastCheckedAppIdRef.current = appId const dismissed = getSandboxMigrationDismissed(appId) // eslint-disable-next-line react/set-state-in-effect setShowMigrationModal(!sandboxEnabled && !dismissed) } }, [appId, isDataReady, sandboxEnabled, setNeedsRuntimeUpgrade]) const renderGraph = useCallback((headerLeftSlot: ReactNode) => { if (!isDataReady) return null return ( ) }, [isDataReady, nodesData, edgesData, data]) if (!isDataReady) { return (
) } const initialFeatures: FeaturesData = buildInitialFeatures(data.features, fileUploadConfigResponse) return ( <> {upgradedFromId && ( )} ) } const WorkflowAppWrapper = () => { return ( ) } export default WorkflowAppWrapper