| 'use client' | 'use client' | ||||
| import Workflow from '@/app/components/workflow' | |||||
| import WorkflowApp from '@/app/components/workflow-app' | |||||
| const Page = () => { | const Page = () => { | ||||
| return ( | return ( | ||||
| <div className='h-full w-full overflow-x-auto'> | <div className='h-full w-full overflow-x-auto'> | ||||
| <Workflow /> | |||||
| <WorkflowApp /> | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| import { | |||||
| memo, | |||||
| useState, | |||||
| } from 'react' | |||||
| import type { EnvironmentVariable } from '@/app/components/workflow/types' | |||||
| import { DSL_EXPORT_CHECK } from '@/app/components/workflow/constants' | |||||
| import { useStore } from '@/app/components/workflow/store' | |||||
| import Features from '@/app/components/workflow/features' | |||||
| import PluginDependency from '@/app/components/workflow/plugin-dependency' | |||||
| import UpdateDSLModal from '@/app/components/workflow/update-dsl-modal' | |||||
| import DSLExportConfirmModal from '@/app/components/workflow/dsl-export-confirm-modal' | |||||
| import { | |||||
| useDSL, | |||||
| usePanelInteractions, | |||||
| } from '@/app/components/workflow/hooks' | |||||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||||
| import WorkflowHeader from './workflow-header' | |||||
| import WorkflowPanel from './workflow-panel' | |||||
| const WorkflowChildren = () => { | |||||
| const { eventEmitter } = useEventEmitterContextContext() | |||||
| const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) | |||||
| const showFeaturesPanel = useStore(s => s.showFeaturesPanel) | |||||
| const showImportDSLModal = useStore(s => s.showImportDSLModal) | |||||
| const setShowImportDSLModal = useStore(s => s.setShowImportDSLModal) | |||||
| const { | |||||
| handlePaneContextmenuCancel, | |||||
| } = usePanelInteractions() | |||||
| const { | |||||
| exportCheck, | |||||
| handleExportDSL, | |||||
| } = useDSL() | |||||
| eventEmitter?.useSubscription((v: any) => { | |||||
| if (v.type === DSL_EXPORT_CHECK) | |||||
| setSecretEnvList(v.payload.data as EnvironmentVariable[]) | |||||
| }) | |||||
| return ( | |||||
| <> | |||||
| <PluginDependency /> | |||||
| { | |||||
| showFeaturesPanel && <Features /> | |||||
| } | |||||
| { | |||||
| showImportDSLModal && ( | |||||
| <UpdateDSLModal | |||||
| onCancel={() => setShowImportDSLModal(false)} | |||||
| onBackup={exportCheck} | |||||
| onImport={handlePaneContextmenuCancel} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| secretEnvList.length > 0 && ( | |||||
| <DSLExportConfirmModal | |||||
| envList={secretEnvList} | |||||
| onConfirm={handleExportDSL} | |||||
| onClose={() => setSecretEnvList([])} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| <WorkflowHeader /> | |||||
| <WorkflowPanel /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default memo(WorkflowChildren) |
| import { memo } from 'react' | |||||
| import ChatVariableButton from '@/app/components/workflow/header/chat-variable-button' | |||||
| import { | |||||
| useNodesReadOnly, | |||||
| } from '@/app/components/workflow/hooks' | |||||
| const ChatVariableTrigger = () => { | |||||
| const { nodesReadOnly } = useNodesReadOnly() | |||||
| return <ChatVariableButton disabled={nodesReadOnly} /> | |||||
| } | |||||
| export default memo(ChatVariableTrigger) |
| import { | |||||
| memo, | |||||
| useCallback, | |||||
| useMemo, | |||||
| } from 'react' | |||||
| import { useNodes } from 'reactflow' | |||||
| import { RiApps2AddLine } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { | |||||
| useStore, | |||||
| useWorkflowStore, | |||||
| } from '@/app/components/workflow/store' | |||||
| import { | |||||
| useChecklistBeforePublish, | |||||
| useNodesReadOnly, | |||||
| useNodesSyncDraft, | |||||
| } from '@/app/components/workflow/hooks' | |||||
| import Button from '@/app/components/base/button' | |||||
| import AppPublisher from '@/app/components/app/app-publisher' | |||||
| import { useFeatures } from '@/app/components/base/features/hooks' | |||||
| import { | |||||
| BlockEnum, | |||||
| InputVarType, | |||||
| } from '@/app/components/workflow/types' | |||||
| import type { StartNodeType } from '@/app/components/workflow/nodes/start/types' | |||||
| import { useToastContext } from '@/app/components/base/toast' | |||||
| import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' | |||||
| import type { PublishWorkflowParams } from '@/types/workflow' | |||||
| import { fetchAppDetail, fetchAppSSO } from '@/service/apps' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import { useSelector as useAppSelector } from '@/context/app-context' | |||||
| const FeaturesTrigger = () => { | |||||
| const { t } = useTranslation() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const appDetail = useAppStore(s => s.appDetail) | |||||
| const appID = appDetail?.id | |||||
| const setAppDetail = useAppStore(s => s.setAppDetail) | |||||
| const systemFeatures = useAppSelector(state => state.systemFeatures) | |||||
| const { | |||||
| nodesReadOnly, | |||||
| getNodesReadOnly, | |||||
| } = useNodesReadOnly() | |||||
| const publishedAt = useStore(s => s.publishedAt) | |||||
| const draftUpdatedAt = useStore(s => s.draftUpdatedAt) | |||||
| const toolPublished = useStore(s => s.toolPublished) | |||||
| const nodes = useNodes<StartNodeType>() | |||||
| const startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| const startVariables = startNode?.data.variables | |||||
| const fileSettings = useFeatures(s => s.features.file) | |||||
| const variables = useMemo(() => { | |||||
| const data = startVariables || [] | |||||
| if (fileSettings?.image?.enabled) { | |||||
| return [ | |||||
| ...data, | |||||
| { | |||||
| type: InputVarType.files, | |||||
| variable: '__image', | |||||
| required: false, | |||||
| label: 'files', | |||||
| }, | |||||
| ] | |||||
| } | |||||
| return data | |||||
| }, [fileSettings?.image?.enabled, startVariables]) | |||||
| const { handleCheckBeforePublish } = useChecklistBeforePublish() | |||||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const { notify } = useToastContext() | |||||
| const handleShowFeatures = useCallback(() => { | |||||
| const { | |||||
| showFeaturesPanel, | |||||
| isRestoring, | |||||
| setShowFeaturesPanel, | |||||
| } = workflowStore.getState() | |||||
| if (getNodesReadOnly() && !isRestoring) | |||||
| return | |||||
| setShowFeaturesPanel(!showFeaturesPanel) | |||||
| }, [workflowStore, getNodesReadOnly]) | |||||
| const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id) | |||||
| const updateAppDetail = useCallback(async () => { | |||||
| try { | |||||
| const res = await fetchAppDetail({ url: '/apps', id: appID! }) | |||||
| if (systemFeatures.enable_web_sso_switch_component) { | |||||
| const ssoRes = await fetchAppSSO({ appId: appID! }) | |||||
| setAppDetail({ ...res, enable_sso: ssoRes.enabled }) | |||||
| } | |||||
| else { | |||||
| setAppDetail({ ...res }) | |||||
| } | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| } | |||||
| }, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component]) | |||||
| const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) | |||||
| const onPublish = useCallback(async (params?: PublishWorkflowParams) => { | |||||
| if (await handleCheckBeforePublish()) { | |||||
| const res = await publishWorkflow({ | |||||
| title: params?.title || '', | |||||
| releaseNotes: params?.releaseNotes || '', | |||||
| }) | |||||
| if (res) { | |||||
| notify({ type: 'success', message: t('common.api.actionSuccess') }) | |||||
| updateAppDetail() | |||||
| workflowStore.getState().setPublishedAt(res.created_at) | |||||
| resetWorkflowVersionHistory() | |||||
| } | |||||
| } | |||||
| else { | |||||
| throw new Error('Checklist failed') | |||||
| } | |||||
| }, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail]) | |||||
| const onPublisherToggle = useCallback((state: boolean) => { | |||||
| if (state) | |||||
| handleSyncWorkflowDraft(true) | |||||
| }, [handleSyncWorkflowDraft]) | |||||
| const handleToolConfigureUpdate = useCallback(() => { | |||||
| workflowStore.setState({ toolPublished: true }) | |||||
| }, [workflowStore]) | |||||
| return ( | |||||
| <> | |||||
| <Button className='text-components-button-secondary-text' onClick={handleShowFeatures}> | |||||
| <RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' /> | |||||
| {t('workflow.common.features')} | |||||
| </Button> | |||||
| <AppPublisher | |||||
| {...{ | |||||
| publishedAt, | |||||
| draftUpdatedAt, | |||||
| disabled: nodesReadOnly, | |||||
| toolPublished, | |||||
| inputs: variables, | |||||
| onRefreshData: handleToolConfigureUpdate, | |||||
| onPublish, | |||||
| onToggle: onPublisherToggle, | |||||
| crossAxisOffset: 4, | |||||
| }} | |||||
| /> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default memo(FeaturesTrigger) |
| import { useMemo } from 'react' | |||||
| import type { HeaderProps } from '@/app/components/workflow/header' | |||||
| import Header from '@/app/components/workflow/header' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import ChatVariableTrigger from './chat-variable-trigger' | |||||
| import FeaturesTrigger from './features-trigger' | |||||
| import { useResetWorkflowVersionHistory } from '@/service/use-workflow' | |||||
| const WorkflowHeader = () => { | |||||
| const appDetail = useAppStore(s => s.appDetail) | |||||
| const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id) | |||||
| const headerProps: HeaderProps = useMemo(() => { | |||||
| return { | |||||
| normal: { | |||||
| components: { | |||||
| left: <ChatVariableTrigger />, | |||||
| middle: <FeaturesTrigger />, | |||||
| }, | |||||
| }, | |||||
| restoring: { | |||||
| onRestoreSettled: resetWorkflowVersionHistory, | |||||
| }, | |||||
| } | |||||
| }, [resetWorkflowVersionHistory]) | |||||
| return ( | |||||
| <Header {...headerProps} /> | |||||
| ) | |||||
| } | |||||
| export default WorkflowHeader |
| import { | |||||
| useCallback, | |||||
| useMemo, | |||||
| } from 'react' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { WorkflowWithInnerContext } from '@/app/components/workflow' | |||||
| import type { WorkflowProps } from '@/app/components/workflow' | |||||
| import WorkflowChildren from './workflow-children' | |||||
| import { | |||||
| useNodesSyncDraft, | |||||
| useWorkflowRun, | |||||
| useWorkflowStartRun, | |||||
| } from '../hooks' | |||||
| type WorkflowMainProps = Pick<WorkflowProps, 'nodes' | 'edges' | 'viewport'> | |||||
| const WorkflowMain = ({ | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| }: WorkflowMainProps) => { | |||||
| const featuresStore = useFeaturesStore() | |||||
| const handleWorkflowDataUpdate = useCallback((payload: any) => { | |||||
| if (payload.features && featuresStore) { | |||||
| const { setFeatures } = featuresStore.getState() | |||||
| setFeatures(payload.features) | |||||
| } | |||||
| }, [featuresStore]) | |||||
| const { | |||||
| doSyncWorkflowDraft, | |||||
| syncWorkflowDraftWhenPageClose, | |||||
| } = useNodesSyncDraft() | |||||
| const { | |||||
| handleBackupDraft, | |||||
| handleLoadBackupDraft, | |||||
| handleRestoreFromPublishedWorkflow, | |||||
| handleRun, | |||||
| handleStopRun, | |||||
| } = useWorkflowRun() | |||||
| const { | |||||
| handleStartWorkflowRun, | |||||
| handleWorkflowStartRunInChatflow, | |||||
| handleWorkflowStartRunInWorkflow, | |||||
| } = useWorkflowStartRun() | |||||
| const hooksStore = useMemo(() => { | |||||
| return { | |||||
| syncWorkflowDraftWhenPageClose, | |||||
| doSyncWorkflowDraft, | |||||
| handleBackupDraft, | |||||
| handleLoadBackupDraft, | |||||
| handleRestoreFromPublishedWorkflow, | |||||
| handleRun, | |||||
| handleStopRun, | |||||
| handleStartWorkflowRun, | |||||
| handleWorkflowStartRunInChatflow, | |||||
| handleWorkflowStartRunInWorkflow, | |||||
| } | |||||
| }, [ | |||||
| syncWorkflowDraftWhenPageClose, | |||||
| doSyncWorkflowDraft, | |||||
| handleBackupDraft, | |||||
| handleLoadBackupDraft, | |||||
| handleRestoreFromPublishedWorkflow, | |||||
| handleRun, | |||||
| handleStopRun, | |||||
| handleStartWorkflowRun, | |||||
| handleWorkflowStartRunInChatflow, | |||||
| handleWorkflowStartRunInWorkflow, | |||||
| ]) | |||||
| return ( | |||||
| <WorkflowWithInnerContext | |||||
| nodes={nodes} | |||||
| edges={edges} | |||||
| viewport={viewport} | |||||
| onWorkflowDataUpdate={handleWorkflowDataUpdate} | |||||
| hooksStore={hooksStore} | |||||
| > | |||||
| <WorkflowChildren /> | |||||
| </WorkflowWithInnerContext> | |||||
| ) | |||||
| } | |||||
| export default WorkflowMain |
| import { useMemo } from 'react' | |||||
| import { useShallow } from 'zustand/react/shallow' | |||||
| import { useStore } from '@/app/components/workflow/store' | |||||
| import { | |||||
| useIsChatMode, | |||||
| } from '../hooks' | |||||
| import DebugAndPreview from '@/app/components/workflow/panel/debug-and-preview' | |||||
| import Record from '@/app/components/workflow/panel/record' | |||||
| import WorkflowPreview from '@/app/components/workflow/panel/workflow-preview' | |||||
| import ChatRecord from '@/app/components/workflow/panel/chat-record' | |||||
| import ChatVariablePanel from '@/app/components/workflow/panel/chat-variable-panel' | |||||
| import GlobalVariablePanel from '@/app/components/workflow/panel/global-variable-panel' | |||||
| import VersionHistoryPanel from '@/app/components/workflow/panel/version-history-panel' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import MessageLogModal from '@/app/components/base/message-log-modal' | |||||
| import type { PanelProps } from '@/app/components/workflow/panel' | |||||
| import Panel from '@/app/components/workflow/panel' | |||||
| const WorkflowPanelOnLeft = () => { | |||||
| const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ | |||||
| currentLogItem: state.currentLogItem, | |||||
| setCurrentLogItem: state.setCurrentLogItem, | |||||
| showMessageLogModal: state.showMessageLogModal, | |||||
| setShowMessageLogModal: state.setShowMessageLogModal, | |||||
| currentLogModalActiveTab: state.currentLogModalActiveTab, | |||||
| }))) | |||||
| return ( | |||||
| <> | |||||
| { | |||||
| showMessageLogModal && ( | |||||
| <MessageLogModal | |||||
| fixedWidth | |||||
| width={400} | |||||
| currentLogItem={currentLogItem} | |||||
| onCancel={() => { | |||||
| setCurrentLogItem() | |||||
| setShowMessageLogModal(false) | |||||
| }} | |||||
| defaultTab={currentLogModalActiveTab} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| ) | |||||
| } | |||||
| const WorkflowPanelOnRight = () => { | |||||
| const isChatMode = useIsChatMode() | |||||
| const historyWorkflowData = useStore(s => s.historyWorkflowData) | |||||
| const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) | |||||
| const showChatVariablePanel = useStore(s => s.showChatVariablePanel) | |||||
| const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel) | |||||
| const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel) | |||||
| return ( | |||||
| <> | |||||
| { | |||||
| historyWorkflowData && !isChatMode && ( | |||||
| <Record /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| historyWorkflowData && isChatMode && ( | |||||
| <ChatRecord /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showDebugAndPreviewPanel && isChatMode && ( | |||||
| <DebugAndPreview /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showDebugAndPreviewPanel && !isChatMode && ( | |||||
| <WorkflowPreview /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showChatVariablePanel && ( | |||||
| <ChatVariablePanel /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showGlobalVariablePanel && ( | |||||
| <GlobalVariablePanel /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showWorkflowVersionHistoryPanel && ( | |||||
| <VersionHistoryPanel/> | |||||
| ) | |||||
| } | |||||
| </> | |||||
| ) | |||||
| } | |||||
| const WorkflowPanel = () => { | |||||
| const panelProps: PanelProps = useMemo(() => { | |||||
| return { | |||||
| components: { | |||||
| left: <WorkflowPanelOnLeft />, | |||||
| right: <WorkflowPanelOnRight />, | |||||
| }, | |||||
| } | |||||
| }, []) | |||||
| return ( | |||||
| <Panel {...panelProps} /> | |||||
| ) | |||||
| } | |||||
| export default WorkflowPanel |
| export * from './use-workflow-init' | |||||
| export * from './use-workflow-template' | |||||
| export * from './use-nodes-sync-draft' | |||||
| export * from './use-workflow-run' | |||||
| export * from './use-workflow-start-run' | |||||
| export * from './use-is-chat-mode' |
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| export const useIsChatMode = () => { | |||||
| const appDetail = useAppStore(s => s.appDetail) | |||||
| return appDetail?.mode === 'advanced-chat' | |||||
| } |
| import { useCallback } from 'react' | |||||
| import produce from 'immer' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| import { useParams } from 'next/navigation' | |||||
| import { | |||||
| useWorkflowStore, | |||||
| } from '@/app/components/workflow/store' | |||||
| import { BlockEnum } from '@/app/components/workflow/types' | |||||
| import { useWorkflowUpdate } from '@/app/components/workflow/hooks' | |||||
| import { | |||||
| useNodesReadOnly, | |||||
| } from '@/app/components/workflow/hooks/use-workflow' | |||||
| import { syncWorkflowDraft } from '@/service/workflow' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { API_PREFIX } from '@/config' | |||||
| export const useNodesSyncDraft = () => { | |||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const { getNodesReadOnly } = useNodesReadOnly() | |||||
| const { handleRefreshWorkflowDraft } = useWorkflowUpdate() | |||||
| const params = useParams() | |||||
| const getPostParams = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| edges, | |||||
| transform, | |||||
| } = store.getState() | |||||
| const [x, y, zoom] = transform | |||||
| const { | |||||
| appId, | |||||
| conversationVariables, | |||||
| environmentVariables, | |||||
| syncWorkflowDraftHash, | |||||
| } = workflowStore.getState() | |||||
| if (appId) { | |||||
| const nodes = getNodes() | |||||
| const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| if (!hasStartNode) | |||||
| return | |||||
| const features = featuresStore!.getState().features | |||||
| const producedNodes = produce(nodes, (draft) => { | |||||
| draft.forEach((node) => { | |||||
| Object.keys(node.data).forEach((key) => { | |||||
| if (key.startsWith('_')) | |||||
| delete node.data[key] | |||||
| }) | |||||
| }) | |||||
| }) | |||||
| const producedEdges = produce(edges, (draft) => { | |||||
| draft.forEach((edge) => { | |||||
| Object.keys(edge.data).forEach((key) => { | |||||
| if (key.startsWith('_')) | |||||
| delete edge.data[key] | |||||
| }) | |||||
| }) | |||||
| }) | |||||
| return { | |||||
| url: `/apps/${appId}/workflows/draft`, | |||||
| params: { | |||||
| graph: { | |||||
| nodes: producedNodes, | |||||
| edges: producedEdges, | |||||
| viewport: { | |||||
| x, | |||||
| y, | |||||
| zoom, | |||||
| }, | |||||
| }, | |||||
| features: { | |||||
| opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '', | |||||
| suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [], | |||||
| suggested_questions_after_answer: features.suggested, | |||||
| text_to_speech: features.text2speech, | |||||
| speech_to_text: features.speech2text, | |||||
| retriever_resource: features.citation, | |||||
| sensitive_word_avoidance: features.moderation, | |||||
| file_upload: features.file, | |||||
| }, | |||||
| environment_variables: environmentVariables, | |||||
| conversation_variables: conversationVariables, | |||||
| hash: syncWorkflowDraftHash, | |||||
| }, | |||||
| } | |||||
| } | |||||
| }, [store, featuresStore, workflowStore]) | |||||
| const syncWorkflowDraftWhenPageClose = useCallback(() => { | |||||
| if (getNodesReadOnly()) | |||||
| return | |||||
| const postParams = getPostParams() | |||||
| if (postParams) { | |||||
| navigator.sendBeacon( | |||||
| `${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`, | |||||
| JSON.stringify(postParams.params), | |||||
| ) | |||||
| } | |||||
| }, [getPostParams, params.appId, getNodesReadOnly]) | |||||
| const doSyncWorkflowDraft = useCallback(async ( | |||||
| notRefreshWhenSyncError?: boolean, | |||||
| callback?: { | |||||
| onSuccess?: () => void | |||||
| onError?: () => void | |||||
| onSettled?: () => void | |||||
| }, | |||||
| ) => { | |||||
| if (getNodesReadOnly()) | |||||
| return | |||||
| const postParams = getPostParams() | |||||
| if (postParams) { | |||||
| const { | |||||
| setSyncWorkflowDraftHash, | |||||
| setDraftUpdatedAt, | |||||
| } = workflowStore.getState() | |||||
| try { | |||||
| const res = await syncWorkflowDraft(postParams) | |||||
| setSyncWorkflowDraftHash(res.hash) | |||||
| setDraftUpdatedAt(res.updated_at) | |||||
| callback?.onSuccess && callback.onSuccess() | |||||
| } | |||||
| catch (error: any) { | |||||
| if (error && error.json && !error.bodyUsed) { | |||||
| error.json().then((err: any) => { | |||||
| if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError) | |||||
| handleRefreshWorkflowDraft() | |||||
| }) | |||||
| } | |||||
| callback?.onError && callback.onError() | |||||
| } | |||||
| finally { | |||||
| callback?.onSettled && callback.onSettled() | |||||
| } | |||||
| } | |||||
| }, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft]) | |||||
| return { | |||||
| doSyncWorkflowDraft, | |||||
| syncWorkflowDraftWhenPageClose, | |||||
| } | |||||
| } |
| import { | |||||
| useCallback, | |||||
| useEffect, | |||||
| useState, | |||||
| } from 'react' | |||||
| import { | |||||
| useStore, | |||||
| useWorkflowStore, | |||||
| } from '@/app/components/workflow/store' | |||||
| import { useWorkflowTemplate } from './use-workflow-template' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import { | |||||
| fetchNodesDefaultConfigs, | |||||
| fetchPublishedWorkflow, | |||||
| fetchWorkflowDraft, | |||||
| syncWorkflowDraft, | |||||
| } from '@/service/workflow' | |||||
| import type { FetchWorkflowDraftResponse } from '@/types/workflow' | |||||
| import { useWorkflowConfig } from '@/service/use-workflow' | |||||
| export const useWorkflowInit = () => { | |||||
| const workflowStore = useWorkflowStore() | |||||
| const { | |||||
| nodes: nodesTemplate, | |||||
| edges: edgesTemplate, | |||||
| } = useWorkflowTemplate() | |||||
| const appDetail = useAppStore(state => state.appDetail)! | |||||
| const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) | |||||
| const [data, setData] = useState<FetchWorkflowDraftResponse>() | |||||
| const [isLoading, setIsLoading] = useState(true) | |||||
| useEffect(() => { | |||||
| workflowStore.setState({ appId: appDetail.id }) | |||||
| }, [appDetail.id, workflowStore]) | |||||
| const handleUpdateWorkflowConfig = useCallback((config: Record<string, any>) => { | |||||
| const { setWorkflowConfig } = workflowStore.getState() | |||||
| setWorkflowConfig(config) | |||||
| }, [workflowStore]) | |||||
| useWorkflowConfig(appDetail.id, handleUpdateWorkflowConfig) | |||||
| const handleGetInitialWorkflowData = useCallback(async () => { | |||||
| try { | |||||
| const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) | |||||
| setData(res) | |||||
| workflowStore.setState({ | |||||
| envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { | |||||
| acc[env.id] = env.value | |||||
| return acc | |||||
| }, {} as Record<string, string>), | |||||
| environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], | |||||
| conversationVariables: res.conversation_variables || [], | |||||
| }) | |||||
| setSyncWorkflowDraftHash(res.hash) | |||||
| setIsLoading(false) | |||||
| } | |||||
| catch (error: any) { | |||||
| if (error && error.json && !error.bodyUsed && appDetail) { | |||||
| error.json().then((err: any) => { | |||||
| if (err.code === 'draft_workflow_not_exist') { | |||||
| workflowStore.setState({ notInitialWorkflow: true }) | |||||
| syncWorkflowDraft({ | |||||
| url: `/apps/${appDetail.id}/workflows/draft`, | |||||
| params: { | |||||
| graph: { | |||||
| nodes: nodesTemplate, | |||||
| edges: edgesTemplate, | |||||
| }, | |||||
| features: { | |||||
| retriever_resource: { enabled: true }, | |||||
| }, | |||||
| environment_variables: [], | |||||
| conversation_variables: [], | |||||
| }, | |||||
| }).then((res) => { | |||||
| workflowStore.getState().setDraftUpdatedAt(res.updated_at) | |||||
| handleGetInitialWorkflowData() | |||||
| }) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) | |||||
| useEffect(() => { | |||||
| handleGetInitialWorkflowData() | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, []) | |||||
| const handleFetchPreloadData = useCallback(async () => { | |||||
| try { | |||||
| const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`) | |||||
| const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`) | |||||
| workflowStore.setState({ | |||||
| nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => { | |||||
| if (!acc[block.type]) | |||||
| acc[block.type] = { ...block.config } | |||||
| return acc | |||||
| }, {} as Record<string, any>), | |||||
| }) | |||||
| workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at) | |||||
| } | |||||
| catch (e) { | |||||
| console.error(e) | |||||
| } | |||||
| }, [workflowStore, appDetail]) | |||||
| useEffect(() => { | |||||
| handleFetchPreloadData() | |||||
| }, [handleFetchPreloadData]) | |||||
| useEffect(() => { | |||||
| if (data) { | |||||
| workflowStore.getState().setDraftUpdatedAt(data.updated_at) | |||||
| workflowStore.getState().setToolPublished(data.tool_published) | |||||
| } | |||||
| }, [data, workflowStore]) | |||||
| return { | |||||
| data, | |||||
| isLoading, | |||||
| } | |||||
| } |
| import { useCallback } from 'react' | |||||
| import { | |||||
| useReactFlow, | |||||
| useStoreApi, | |||||
| } from 'reactflow' | |||||
| import produce from 'immer' | |||||
| import { v4 as uuidV4 } from 'uuid' | |||||
| import { usePathname } from 'next/navigation' | |||||
| import { useWorkflowStore } from '@/app/components/workflow/store' | |||||
| import { WorkflowRunningStatus } from '@/app/components/workflow/types' | |||||
| import { useWorkflowUpdate } from '@/app/components/workflow/hooks/use-workflow-interactions' | |||||
| import { useWorkflowRunEvent } from '@/app/components/workflow/hooks/use-workflow-run-event/use-workflow-run-event' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import type { IOtherOptions } from '@/service/base' | |||||
| import { ssePost } from '@/service/base' | |||||
| import { stopWorkflowRun } from '@/service/workflow' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' | |||||
| import type { VersionHistory } from '@/types/workflow' | |||||
| import { noop } from 'lodash-es' | |||||
| import { useNodesSyncDraft } from './use-nodes-sync-draft' | |||||
| export const useWorkflowRun = () => { | |||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const reactflow = useReactFlow() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const { doSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() | |||||
| const pathname = usePathname() | |||||
| const { | |||||
| handleWorkflowStarted, | |||||
| handleWorkflowFinished, | |||||
| handleWorkflowFailed, | |||||
| handleWorkflowNodeStarted, | |||||
| handleWorkflowNodeFinished, | |||||
| handleWorkflowNodeIterationStarted, | |||||
| handleWorkflowNodeIterationNext, | |||||
| handleWorkflowNodeIterationFinished, | |||||
| handleWorkflowNodeLoopStarted, | |||||
| handleWorkflowNodeLoopNext, | |||||
| handleWorkflowNodeLoopFinished, | |||||
| handleWorkflowNodeRetry, | |||||
| handleWorkflowAgentLog, | |||||
| handleWorkflowTextChunk, | |||||
| handleWorkflowTextReplace, | |||||
| } = useWorkflowRunEvent() | |||||
| const handleBackupDraft = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| edges, | |||||
| } = store.getState() | |||||
| const { getViewport } = reactflow | |||||
| const { | |||||
| backupDraft, | |||||
| setBackupDraft, | |||||
| environmentVariables, | |||||
| } = workflowStore.getState() | |||||
| const { features } = featuresStore!.getState() | |||||
| if (!backupDraft) { | |||||
| setBackupDraft({ | |||||
| nodes: getNodes(), | |||||
| edges, | |||||
| viewport: getViewport(), | |||||
| features, | |||||
| environmentVariables, | |||||
| }) | |||||
| doSyncWorkflowDraft() | |||||
| } | |||||
| }, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft]) | |||||
| const handleLoadBackupDraft = useCallback(() => { | |||||
| const { | |||||
| backupDraft, | |||||
| setBackupDraft, | |||||
| setEnvironmentVariables, | |||||
| } = workflowStore.getState() | |||||
| if (backupDraft) { | |||||
| const { | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| features, | |||||
| environmentVariables, | |||||
| } = backupDraft | |||||
| handleUpdateWorkflowCanvas({ | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| }) | |||||
| setEnvironmentVariables(environmentVariables) | |||||
| featuresStore!.setState({ features }) | |||||
| setBackupDraft(undefined) | |||||
| } | |||||
| }, [handleUpdateWorkflowCanvas, workflowStore, featuresStore]) | |||||
| 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 | |||||
| node.data._runningStatus = undefined | |||||
| }) | |||||
| }) | |||||
| setNodes(newNodes) | |||||
| await doSyncWorkflowDraft() | |||||
| const { | |||||
| onWorkflowStarted, | |||||
| onWorkflowFinished, | |||||
| onNodeStarted, | |||||
| onNodeFinished, | |||||
| onIterationStart, | |||||
| onIterationNext, | |||||
| onIterationFinish, | |||||
| onLoopStart, | |||||
| onLoopNext, | |||||
| onLoopFinish, | |||||
| onNodeRetry, | |||||
| onAgentLog, | |||||
| onError, | |||||
| ...restCallback | |||||
| } = callback || {} | |||||
| workflowStore.setState({ historyWorkflowData: undefined }) | |||||
| const appDetail = useAppStore.getState().appDetail | |||||
| const workflowContainer = document.getElementById('workflow-container') | |||||
| const { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| } = workflowContainer! | |||||
| let url = '' | |||||
| if (appDetail?.mode === 'advanced-chat') | |||||
| url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run` | |||||
| if (appDetail?.mode === 'workflow') | |||||
| url = `/apps/${appDetail.id}/workflows/draft/run` | |||||
| const { | |||||
| setWorkflowRunningData, | |||||
| } = workflowStore.getState() | |||||
| setWorkflowRunningData({ | |||||
| result: { | |||||
| status: WorkflowRunningStatus.Running, | |||||
| }, | |||||
| tracing: [], | |||||
| resultText: '', | |||||
| }) | |||||
| let ttsUrl = '' | |||||
| let ttsIsPublic = false | |||||
| if (params.token) { | |||||
| ttsUrl = '/text-to-audio' | |||||
| ttsIsPublic = true | |||||
| } | |||||
| else if (params.appId) { | |||||
| if (pathname.search('explore/installed') > -1) | |||||
| ttsUrl = `/installed-apps/${params.appId}/text-to-audio` | |||||
| else | |||||
| ttsUrl = `/apps/${params.appId}/text-to-audio` | |||||
| } | |||||
| const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', noop) | |||||
| ssePost( | |||||
| url, | |||||
| { | |||||
| body: params, | |||||
| }, | |||||
| { | |||||
| onWorkflowStarted: (params) => { | |||||
| handleWorkflowStarted(params) | |||||
| if (onWorkflowStarted) | |||||
| onWorkflowStarted(params) | |||||
| }, | |||||
| onWorkflowFinished: (params) => { | |||||
| handleWorkflowFinished(params) | |||||
| if (onWorkflowFinished) | |||||
| onWorkflowFinished(params) | |||||
| }, | |||||
| onError: (params) => { | |||||
| handleWorkflowFailed() | |||||
| if (onError) | |||||
| onError(params) | |||||
| }, | |||||
| onNodeStarted: (params) => { | |||||
| handleWorkflowNodeStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onNodeStarted) | |||||
| onNodeStarted(params) | |||||
| }, | |||||
| onNodeFinished: (params) => { | |||||
| handleWorkflowNodeFinished(params) | |||||
| if (onNodeFinished) | |||||
| onNodeFinished(params) | |||||
| }, | |||||
| onIterationStart: (params) => { | |||||
| handleWorkflowNodeIterationStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onIterationStart) | |||||
| onIterationStart(params) | |||||
| }, | |||||
| onIterationNext: (params) => { | |||||
| handleWorkflowNodeIterationNext(params) | |||||
| if (onIterationNext) | |||||
| onIterationNext(params) | |||||
| }, | |||||
| onIterationFinish: (params) => { | |||||
| handleWorkflowNodeIterationFinished(params) | |||||
| if (onIterationFinish) | |||||
| onIterationFinish(params) | |||||
| }, | |||||
| onLoopStart: (params) => { | |||||
| handleWorkflowNodeLoopStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onLoopStart) | |||||
| onLoopStart(params) | |||||
| }, | |||||
| onLoopNext: (params) => { | |||||
| handleWorkflowNodeLoopNext(params) | |||||
| if (onLoopNext) | |||||
| onLoopNext(params) | |||||
| }, | |||||
| onLoopFinish: (params) => { | |||||
| handleWorkflowNodeLoopFinished(params) | |||||
| if (onLoopFinish) | |||||
| onLoopFinish(params) | |||||
| }, | |||||
| onNodeRetry: (params) => { | |||||
| handleWorkflowNodeRetry(params) | |||||
| if (onNodeRetry) | |||||
| onNodeRetry(params) | |||||
| }, | |||||
| onAgentLog: (params) => { | |||||
| handleWorkflowAgentLog(params) | |||||
| if (onAgentLog) | |||||
| onAgentLog(params) | |||||
| }, | |||||
| onTextChunk: (params) => { | |||||
| handleWorkflowTextChunk(params) | |||||
| }, | |||||
| onTextReplace: (params) => { | |||||
| handleWorkflowTextReplace(params) | |||||
| }, | |||||
| onTTSChunk: (messageId: string, audio: string) => { | |||||
| if (!audio || audio === '') | |||||
| return | |||||
| player.playAudioWithAudio(audio, true) | |||||
| AudioPlayerManager.getInstance().resetMsgId(messageId) | |||||
| }, | |||||
| onTTSEnd: (messageId: string, audio: string) => { | |||||
| player.playAudioWithAudio(audio, false) | |||||
| }, | |||||
| ...restCallback, | |||||
| }, | |||||
| ) | |||||
| }, [ | |||||
| store, | |||||
| workflowStore, | |||||
| doSyncWorkflowDraft, | |||||
| handleWorkflowStarted, | |||||
| handleWorkflowFinished, | |||||
| handleWorkflowFailed, | |||||
| handleWorkflowNodeStarted, | |||||
| handleWorkflowNodeFinished, | |||||
| handleWorkflowNodeIterationStarted, | |||||
| handleWorkflowNodeIterationNext, | |||||
| handleWorkflowNodeIterationFinished, | |||||
| handleWorkflowNodeLoopStarted, | |||||
| handleWorkflowNodeLoopNext, | |||||
| handleWorkflowNodeLoopFinished, | |||||
| handleWorkflowNodeRetry, | |||||
| handleWorkflowTextChunk, | |||||
| handleWorkflowTextReplace, | |||||
| handleWorkflowAgentLog, | |||||
| pathname], | |||||
| ) | |||||
| const handleStopRun = useCallback((taskId: string) => { | |||||
| const appId = useAppStore.getState().appDetail?.id | |||||
| stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`) | |||||
| }, []) | |||||
| const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => { | |||||
| const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } })) | |||||
| const edges = publishedWorkflow.graph.edges | |||||
| const viewport = publishedWorkflow.graph.viewport! | |||||
| handleUpdateWorkflowCanvas({ | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| }) | |||||
| const mappedFeatures = { | |||||
| opening: { | |||||
| enabled: !!publishedWorkflow.features.opening_statement || !!publishedWorkflow.features.suggested_questions.length, | |||||
| opening_statement: publishedWorkflow.features.opening_statement, | |||||
| suggested_questions: publishedWorkflow.features.suggested_questions, | |||||
| }, | |||||
| suggested: publishedWorkflow.features.suggested_questions_after_answer, | |||||
| text2speech: publishedWorkflow.features.text_to_speech, | |||||
| speech2text: publishedWorkflow.features.speech_to_text, | |||||
| citation: publishedWorkflow.features.retriever_resource, | |||||
| moderation: publishedWorkflow.features.sensitive_word_avoidance, | |||||
| file: publishedWorkflow.features.file_upload, | |||||
| } | |||||
| featuresStore?.setState({ features: mappedFeatures }) | |||||
| workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || []) | |||||
| }, [featuresStore, handleUpdateWorkflowCanvas, workflowStore]) | |||||
| return { | |||||
| handleBackupDraft, | |||||
| handleLoadBackupDraft, | |||||
| handleRun, | |||||
| handleStopRun, | |||||
| handleRestoreFromPublishedWorkflow, | |||||
| } | |||||
| } |
| import { useCallback } from 'react' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| import { useWorkflowStore } from '@/app/components/workflow/store' | |||||
| import { | |||||
| BlockEnum, | |||||
| WorkflowRunningStatus, | |||||
| } from '@/app/components/workflow/types' | |||||
| import { useWorkflowInteractions } from '@/app/components/workflow/hooks' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { | |||||
| useIsChatMode, | |||||
| useNodesSyncDraft, | |||||
| useWorkflowRun, | |||||
| } from '.' | |||||
| export const useWorkflowStartRun = () => { | |||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const isChatMode = useIsChatMode() | |||||
| const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() | |||||
| const { handleRun } = useWorkflowRun() | |||||
| const { doSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const handleWorkflowStartRunInWorkflow = useCallback(async () => { | |||||
| const { | |||||
| workflowRunningData, | |||||
| } = workflowStore.getState() | |||||
| if (workflowRunningData?.result.status === WorkflowRunningStatus.Running) | |||||
| return | |||||
| const { getNodes } = store.getState() | |||||
| const nodes = getNodes() | |||||
| const startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| const startVariables = startNode?.data.variables || [] | |||||
| const fileSettings = featuresStore!.getState().features.file | |||||
| const { | |||||
| showDebugAndPreviewPanel, | |||||
| setShowDebugAndPreviewPanel, | |||||
| setShowInputsPanel, | |||||
| setShowEnvPanel, | |||||
| } = workflowStore.getState() | |||||
| setShowEnvPanel(false) | |||||
| if (showDebugAndPreviewPanel) { | |||||
| handleCancelDebugAndPreviewPanel() | |||||
| return | |||||
| } | |||||
| if (!startVariables.length && !fileSettings?.image?.enabled) { | |||||
| await doSyncWorkflowDraft() | |||||
| handleRun({ inputs: {}, files: [] }) | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setShowInputsPanel(false) | |||||
| } | |||||
| else { | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setShowInputsPanel(true) | |||||
| } | |||||
| }, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft]) | |||||
| const handleWorkflowStartRunInChatflow = useCallback(async () => { | |||||
| const { | |||||
| showDebugAndPreviewPanel, | |||||
| setShowDebugAndPreviewPanel, | |||||
| setHistoryWorkflowData, | |||||
| setShowEnvPanel, | |||||
| setShowChatVariablePanel, | |||||
| } = workflowStore.getState() | |||||
| setShowEnvPanel(false) | |||||
| setShowChatVariablePanel(false) | |||||
| if (showDebugAndPreviewPanel) | |||||
| handleCancelDebugAndPreviewPanel() | |||||
| else | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setHistoryWorkflowData(undefined) | |||||
| }, [workflowStore, handleCancelDebugAndPreviewPanel]) | |||||
| const handleStartWorkflowRun = useCallback(() => { | |||||
| if (!isChatMode) | |||||
| handleWorkflowStartRunInWorkflow() | |||||
| else | |||||
| handleWorkflowStartRunInChatflow() | |||||
| }, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow]) | |||||
| return { | |||||
| handleStartWorkflowRun, | |||||
| handleWorkflowStartRunInWorkflow, | |||||
| handleWorkflowStartRunInChatflow, | |||||
| } | |||||
| } |
| import { generateNewNode } from '../utils' | |||||
| import { generateNewNode } from '@/app/components/workflow/utils' | |||||
| import { | import { | ||||
| NODE_WIDTH_X_OFFSET, | NODE_WIDTH_X_OFFSET, | ||||
| START_INITIAL_POSITION, | START_INITIAL_POSITION, | ||||
| } from '../constants' | |||||
| import { useIsChatMode } from './use-workflow' | |||||
| import { useNodesInitialData } from './use-nodes-data' | |||||
| } from '@/app/components/workflow/constants' | |||||
| import { useNodesInitialData } from '@/app/components/workflow/hooks' | |||||
| import { useIsChatMode } from './use-is-chat-mode' | |||||
| export const useWorkflowTemplate = () => { | export const useWorkflowTemplate = () => { | ||||
| const isChatMode = useIsChatMode() | const isChatMode = useIsChatMode() |
| import { | |||||
| useMemo, | |||||
| } from 'react' | |||||
| import useSWR from 'swr' | |||||
| import { | |||||
| SupportUploadFileTypes, | |||||
| } from '@/app/components/workflow/types' | |||||
| import { | |||||
| useWorkflowInit, | |||||
| } from './hooks' | |||||
| import { | |||||
| initialEdges, | |||||
| initialNodes, | |||||
| } from '@/app/components/workflow/utils' | |||||
| 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 { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' | |||||
| import { fetchFileUploadConfig } from '@/service/common' | |||||
| import WorkflowWithDefaultContext from '@/app/components/workflow' | |||||
| import { | |||||
| WorkflowContextProvider, | |||||
| } from '@/app/components/workflow/context' | |||||
| import { createWorkflowSlice } from './store/workflow/workflow-slice' | |||||
| import WorkflowAppMain from './components/workflow-main' | |||||
| const WorkflowAppWithAdditionalContext = () => { | |||||
| 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 ( | |||||
| <div className='relative flex h-full w-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| 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 ( | |||||
| <WorkflowWithDefaultContext | |||||
| edges={edgesData} | |||||
| nodes={nodesData} | |||||
| > | |||||
| <FeaturesProvider features={initialFeatures}> | |||||
| <WorkflowAppMain | |||||
| nodes={nodesData} | |||||
| edges={edgesData} | |||||
| viewport={data.graph.viewport} | |||||
| /> | |||||
| </FeaturesProvider> | |||||
| </WorkflowWithDefaultContext> | |||||
| ) | |||||
| } | |||||
| const WorkflowAppWrapper = () => { | |||||
| return ( | |||||
| <WorkflowContextProvider | |||||
| injectWorkflowStoreSliceFn={createWorkflowSlice} | |||||
| > | |||||
| <WorkflowAppWithAdditionalContext /> | |||||
| </WorkflowContextProvider> | |||||
| ) | |||||
| } | |||||
| export default WorkflowAppWrapper |
| import type { StateCreator } from 'zustand' | |||||
| export type WorkflowSliceShape = { | |||||
| appId: string | |||||
| notInitialWorkflow: boolean | |||||
| setNotInitialWorkflow: (notInitialWorkflow: boolean) => void | |||||
| nodesDefaultConfigs: Record<string, any> | |||||
| setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void | |||||
| } | |||||
| export type CreateWorkflowSlice = StateCreator<WorkflowSliceShape> | |||||
| export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({ | |||||
| appId: '', | |||||
| notInitialWorkflow: false, | |||||
| setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), | |||||
| nodesDefaultConfigs: {}, | |||||
| setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), | |||||
| }) |
| createContext, | createContext, | ||||
| useRef, | useRef, | ||||
| } from 'react' | } from 'react' | ||||
| import { createWorkflowStore } from './store' | |||||
| import { | |||||
| createWorkflowStore, | |||||
| } from './store' | |||||
| import type { StateCreator } from 'zustand' | |||||
| import type { WorkflowSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice' | |||||
| type WorkflowStore = ReturnType<typeof createWorkflowStore> | type WorkflowStore = ReturnType<typeof createWorkflowStore> | ||||
| export const WorkflowContext = createContext<WorkflowStore | null>(null) | export const WorkflowContext = createContext<WorkflowStore | null>(null) | ||||
| type WorkflowProviderProps = { | |||||
| export type WorkflowProviderProps = { | |||||
| children: React.ReactNode | children: React.ReactNode | ||||
| injectWorkflowStoreSliceFn?: StateCreator<WorkflowSliceShape> | |||||
| } | } | ||||
| export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => { | |||||
| export const WorkflowContextProvider = ({ children, injectWorkflowStoreSliceFn }: WorkflowProviderProps) => { | |||||
| const storeRef = useRef<WorkflowStore | undefined>(undefined) | const storeRef = useRef<WorkflowStore | undefined>(undefined) | ||||
| if (!storeRef.current) | if (!storeRef.current) | ||||
| storeRef.current = createWorkflowStore() | |||||
| storeRef.current = createWorkflowStore({ injectWorkflowStoreSliceFn }) | |||||
| return ( | return ( | ||||
| <WorkflowContext.Provider value={storeRef.current}> | <WorkflowContext.Provider value={storeRef.current}> |
| import { memo } from 'react' | import { memo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useWorkflow } from '../hooks' | |||||
| import { useFormatTimeFromNow } from '../hooks' | |||||
| import { useStore } from '@/app/components/workflow/store' | import { useStore } from '@/app/components/workflow/store' | ||||
| import useTimestamp from '@/hooks/use-timestamp' | import useTimestamp from '@/hooks/use-timestamp' | ||||
| const EditingTitle = () => { | const EditingTitle = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { formatTime } = useTimestamp() | const { formatTime } = useTimestamp() | ||||
| const { formatTimeFromNow } = useWorkflow() | |||||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||||
| const draftUpdatedAt = useStore(state => state.draftUpdatedAt) | const draftUpdatedAt = useStore(state => state.draftUpdatedAt) | ||||
| const publishedAt = useStore(state => state.publishedAt) | const publishedAt = useStore(state => state.publishedAt) | ||||
| const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft) | const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft) |
| import { | |||||
| useCallback, | |||||
| } from 'react' | |||||
| import { useNodes } from 'reactflow' | |||||
| import { | |||||
| useStore, | |||||
| useWorkflowStore, | |||||
| } from '../store' | |||||
| import type { StartNodeType } from '../nodes/start/types' | |||||
| import { | |||||
| useNodesInteractions, | |||||
| useNodesReadOnly, | |||||
| useWorkflowRun, | |||||
| } from '../hooks' | |||||
| import Divider from '../../base/divider' | |||||
| import RunAndHistory from './run-and-history' | |||||
| import EditingTitle from './editing-title' | |||||
| import EnvButton from './env-button' | |||||
| import VersionHistoryButton from './version-history-button' | |||||
| export type HeaderInNormalProps = { | |||||
| components?: { | |||||
| left?: React.ReactNode | |||||
| middle?: React.ReactNode | |||||
| } | |||||
| } | |||||
| const HeaderInNormal = ({ | |||||
| components, | |||||
| }: HeaderInNormalProps) => { | |||||
| const workflowStore = useWorkflowStore() | |||||
| const { nodesReadOnly } = useNodesReadOnly() | |||||
| const { handleNodeSelect } = useNodesInteractions() | |||||
| const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) | |||||
| const setShowEnvPanel = useStore(s => s.setShowEnvPanel) | |||||
| const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel) | |||||
| const nodes = useNodes<StartNodeType>() | |||||
| const selectedNode = nodes.find(node => node.data.selected) | |||||
| const { handleBackupDraft } = useWorkflowRun() | |||||
| const onStartRestoring = useCallback(() => { | |||||
| workflowStore.setState({ isRestoring: true }) | |||||
| handleBackupDraft() | |||||
| // clear right panel | |||||
| if (selectedNode) | |||||
| handleNodeSelect(selectedNode.id, true) | |||||
| setShowWorkflowVersionHistoryPanel(true) | |||||
| setShowEnvPanel(false) | |||||
| setShowDebugAndPreviewPanel(false) | |||||
| }, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode, | |||||
| setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel]) | |||||
| return ( | |||||
| <> | |||||
| <div> | |||||
| <EditingTitle /> | |||||
| </div> | |||||
| <div className='flex items-center gap-2'> | |||||
| {components?.left} | |||||
| <EnvButton disabled={nodesReadOnly} /> | |||||
| <Divider type='vertical' className='mx-auto h-3.5' /> | |||||
| <RunAndHistory /> | |||||
| {components?.middle} | |||||
| <VersionHistoryButton onClick={onStartRestoring} /> | |||||
| </div> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default HeaderInNormal |
| import { | |||||
| useCallback, | |||||
| } from 'react' | |||||
| import { RiHistoryLine } from '@remixicon/react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { | |||||
| useStore, | |||||
| useWorkflowStore, | |||||
| } from '../store' | |||||
| import { | |||||
| WorkflowVersion, | |||||
| } from '../types' | |||||
| import { | |||||
| useNodesSyncDraft, | |||||
| useWorkflowRun, | |||||
| } from '../hooks' | |||||
| import Toast from '../../base/toast' | |||||
| import RestoringTitle from './restoring-title' | |||||
| import Button from '@/app/components/base/button' | |||||
| export type HeaderInRestoringProps = { | |||||
| onRestoreSettled?: () => void | |||||
| } | |||||
| const HeaderInRestoring = ({ | |||||
| onRestoreSettled, | |||||
| }: HeaderInRestoringProps) => { | |||||
| const { t } = useTranslation() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const currentVersion = useStore(s => s.currentVersion) | |||||
| const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) | |||||
| const { | |||||
| handleLoadBackupDraft, | |||||
| } = useWorkflowRun() | |||||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const handleCancelRestore = useCallback(() => { | |||||
| handleLoadBackupDraft() | |||||
| workflowStore.setState({ isRestoring: false }) | |||||
| setShowWorkflowVersionHistoryPanel(false) | |||||
| }, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel]) | |||||
| const handleRestore = useCallback(() => { | |||||
| setShowWorkflowVersionHistoryPanel(false) | |||||
| workflowStore.setState({ isRestoring: false }) | |||||
| workflowStore.setState({ backupDraft: undefined }) | |||||
| handleSyncWorkflowDraft(true, false, { | |||||
| onSuccess: () => { | |||||
| Toast.notify({ | |||||
| type: 'success', | |||||
| message: t('workflow.versionHistory.action.restoreSuccess'), | |||||
| }) | |||||
| }, | |||||
| onError: () => { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('workflow.versionHistory.action.restoreFailure'), | |||||
| }) | |||||
| }, | |||||
| onSettled: () => { | |||||
| onRestoreSettled?.() | |||||
| }, | |||||
| }) | |||||
| }, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, onRestoreSettled, t]) | |||||
| return ( | |||||
| <> | |||||
| <div> | |||||
| <RestoringTitle /> | |||||
| </div> | |||||
| <div className='flex items-center justify-end gap-x-2'> | |||||
| <Button | |||||
| onClick={handleRestore} | |||||
| disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft} | |||||
| variant='primary' | |||||
| > | |||||
| {t('workflow.common.restore')} | |||||
| </Button> | |||||
| <Button | |||||
| className='text-components-button-secondary-accent-text' | |||||
| onClick={handleCancelRestore} | |||||
| > | |||||
| <div className='flex items-center gap-x-0.5'> | |||||
| <RiHistoryLine className='h-4 w-4' /> | |||||
| <span className='px-0.5'>{t('workflow.common.exitVersions')}</span> | |||||
| </div> | |||||
| </Button> | |||||
| </div> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default HeaderInRestoring |
| import { | |||||
| useCallback, | |||||
| } from 'react' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { | |||||
| useWorkflowStore, | |||||
| } from '../store' | |||||
| import { | |||||
| useWorkflowRun, | |||||
| } from '../hooks' | |||||
| import Divider from '../../base/divider' | |||||
| import RunningTitle from './running-title' | |||||
| import ViewHistory from './view-history' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' | |||||
| const HeaderInHistory = () => { | |||||
| const { t } = useTranslation() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const { | |||||
| handleLoadBackupDraft, | |||||
| } = useWorkflowRun() | |||||
| const handleGoBackToEdit = useCallback(() => { | |||||
| handleLoadBackupDraft() | |||||
| workflowStore.setState({ historyWorkflowData: undefined }) | |||||
| }, [workflowStore, handleLoadBackupDraft]) | |||||
| return ( | |||||
| <> | |||||
| <div> | |||||
| <RunningTitle /> | |||||
| </div> | |||||
| <div className='flex items-center space-x-2'> | |||||
| <ViewHistory withText /> | |||||
| <Divider type='vertical' className='mx-auto h-3.5' /> | |||||
| <Button | |||||
| variant='primary' | |||||
| onClick={handleGoBackToEdit} | |||||
| > | |||||
| <ArrowNarrowLeft className='mr-1 h-4 w-4' /> | |||||
| {t('workflow.common.goBackToEdit')} | |||||
| </Button> | |||||
| </div> | |||||
| </> | |||||
| ) | |||||
| } | |||||
| export default HeaderInHistory |
| import type { FC } from 'react' | |||||
| import { | import { | ||||
| memo, | |||||
| useCallback, | |||||
| useMemo, | |||||
| } from 'react' | |||||
| import { RiApps2AddLine, RiHistoryLine } from '@remixicon/react' | |||||
| import { useNodes } from 'reactflow' | |||||
| import { useTranslation } from 'react-i18next' | |||||
| import { useContext, useContextSelector } from 'use-context-selector' | |||||
| import { | |||||
| useStore, | |||||
| useWorkflowStore, | |||||
| } from '../store' | |||||
| import { | |||||
| BlockEnum, | |||||
| InputVarType, | |||||
| WorkflowVersion, | |||||
| } from '../types' | |||||
| import type { StartNodeType } from '../nodes/start/types' | |||||
| import { | |||||
| useChecklistBeforePublish, | |||||
| useIsChatMode, | |||||
| useNodesInteractions, | |||||
| useNodesReadOnly, | |||||
| useNodesSyncDraft, | |||||
| useWorkflowMode, | useWorkflowMode, | ||||
| useWorkflowRun, | |||||
| } from '../hooks' | } from '../hooks' | ||||
| import AppPublisher from '../../app/app-publisher' | |||||
| import Toast, { ToastContext } from '../../base/toast' | |||||
| import Divider from '../../base/divider' | |||||
| 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 ChatVariableButton from './chat-variable-button' | |||||
| import EnvButton from './env-button' | |||||
| import VersionHistoryButton from './version-history-button' | |||||
| import Button from '@/app/components/base/button' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows' | |||||
| import { useFeatures } from '@/app/components/base/features/hooks' | |||||
| import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow' | |||||
| import type { PublishWorkflowParams } from '@/types/workflow' | |||||
| import { fetchAppDetail, fetchAppSSO } from '@/service/apps' | |||||
| import AppContext from '@/context/app-context' | |||||
| const Header: FC = () => { | |||||
| const { t } = useTranslation() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const appDetail = useAppStore(s => s.appDetail) | |||||
| const setAppDetail = useAppStore(s => s.setAppDetail) | |||||
| const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures) | |||||
| const appID = appDetail?.id | |||||
| const isChatMode = useIsChatMode() | |||||
| const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly() | |||||
| const { handleNodeSelect } = useNodesInteractions() | |||||
| const publishedAt = useStore(s => s.publishedAt) | |||||
| const draftUpdatedAt = useStore(s => s.draftUpdatedAt) | |||||
| const toolPublished = useStore(s => s.toolPublished) | |||||
| const currentVersion = useStore(s => s.currentVersion) | |||||
| const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel) | |||||
| const setShowEnvPanel = useStore(s => s.setShowEnvPanel) | |||||
| const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel) | |||||
| const nodes = useNodes<StartNodeType>() | |||||
| const startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| const selectedNode = nodes.find(node => node.data.selected) | |||||
| const startVariables = startNode?.data.variables | |||||
| const fileSettings = useFeatures(s => s.features.file) | |||||
| const variables = useMemo(() => { | |||||
| const data = startVariables || [] | |||||
| if (fileSettings?.image?.enabled) { | |||||
| return [ | |||||
| ...data, | |||||
| { | |||||
| type: InputVarType.files, | |||||
| variable: '__image', | |||||
| required: false, | |||||
| label: 'files', | |||||
| }, | |||||
| ] | |||||
| } | |||||
| return data | |||||
| }, [fileSettings?.image?.enabled, startVariables]) | |||||
| const { | |||||
| handleLoadBackupDraft, | |||||
| handleBackupDraft, | |||||
| } = useWorkflowRun() | |||||
| const { handleCheckBeforePublish } = useChecklistBeforePublish() | |||||
| const { handleSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const { notify } = useContext(ToastContext) | |||||
| import type { HeaderInNormalProps } from './header-in-normal' | |||||
| import HeaderInNormal from './header-in-normal' | |||||
| import HeaderInHistory from './header-in-view-history' | |||||
| import type { HeaderInRestoringProps } from './header-in-restoring' | |||||
| import HeaderInRestoring from './header-in-restoring' | |||||
| export type HeaderProps = { | |||||
| normal?: HeaderInNormalProps | |||||
| restoring?: HeaderInRestoringProps | |||||
| } | |||||
| const Header = ({ | |||||
| normal: normalProps, | |||||
| restoring: restoringProps, | |||||
| }: HeaderProps) => { | |||||
| const { | const { | ||||
| normal, | normal, | ||||
| restoring, | restoring, | ||||
| viewHistory, | viewHistory, | ||||
| } = useWorkflowMode() | } = useWorkflowMode() | ||||
| const handleShowFeatures = useCallback(() => { | |||||
| const { | |||||
| showFeaturesPanel, | |||||
| isRestoring, | |||||
| setShowFeaturesPanel, | |||||
| } = workflowStore.getState() | |||||
| if (getNodesReadOnly() && !isRestoring) | |||||
| return | |||||
| setShowFeaturesPanel(!showFeaturesPanel) | |||||
| }, [workflowStore, getNodesReadOnly]) | |||||
| const handleCancelRestore = useCallback(() => { | |||||
| handleLoadBackupDraft() | |||||
| workflowStore.setState({ isRestoring: false }) | |||||
| setShowWorkflowVersionHistoryPanel(false) | |||||
| }, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel]) | |||||
| const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id) | |||||
| const handleRestore = useCallback(() => { | |||||
| setShowWorkflowVersionHistoryPanel(false) | |||||
| workflowStore.setState({ isRestoring: false }) | |||||
| workflowStore.setState({ backupDraft: undefined }) | |||||
| handleSyncWorkflowDraft(true, false, { | |||||
| onSuccess: () => { | |||||
| Toast.notify({ | |||||
| type: 'success', | |||||
| message: t('workflow.versionHistory.action.restoreSuccess'), | |||||
| }) | |||||
| }, | |||||
| onError: () => { | |||||
| Toast.notify({ | |||||
| type: 'error', | |||||
| message: t('workflow.versionHistory.action.restoreFailure'), | |||||
| }) | |||||
| }, | |||||
| onSettled: () => { | |||||
| resetWorkflowVersionHistory() | |||||
| }, | |||||
| }) | |||||
| }, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, resetWorkflowVersionHistory, t]) | |||||
| const updateAppDetail = useCallback(async () => { | |||||
| try { | |||||
| const res = await fetchAppDetail({ url: '/apps', id: appID! }) | |||||
| if (systemFeatures.enable_web_sso_switch_component) { | |||||
| const ssoRes = await fetchAppSSO({ appId: appID! }) | |||||
| setAppDetail({ ...res, enable_sso: ssoRes.enabled }) | |||||
| } | |||||
| else { | |||||
| setAppDetail({ ...res }) | |||||
| } | |||||
| } | |||||
| catch (error) { | |||||
| console.error(error) | |||||
| } | |||||
| }, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component]) | |||||
| const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!) | |||||
| const onPublish = useCallback(async (params?: PublishWorkflowParams) => { | |||||
| if (await handleCheckBeforePublish()) { | |||||
| const res = await publishWorkflow({ | |||||
| title: params?.title || '', | |||||
| releaseNotes: params?.releaseNotes || '', | |||||
| }) | |||||
| if (res) { | |||||
| notify({ type: 'success', message: t('common.api.actionSuccess') }) | |||||
| updateAppDetail() | |||||
| workflowStore.getState().setPublishedAt(res.created_at) | |||||
| resetWorkflowVersionHistory() | |||||
| } | |||||
| } | |||||
| else { | |||||
| throw new Error('Checklist failed') | |||||
| } | |||||
| }, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail]) | |||||
| const onStartRestoring = useCallback(() => { | |||||
| workflowStore.setState({ isRestoring: true }) | |||||
| handleBackupDraft() | |||||
| // clear right panel | |||||
| if (selectedNode) | |||||
| handleNodeSelect(selectedNode.id, true) | |||||
| setShowWorkflowVersionHistoryPanel(true) | |||||
| setShowEnvPanel(false) | |||||
| setShowDebugAndPreviewPanel(false) | |||||
| }, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode, | |||||
| setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel]) | |||||
| const onPublisherToggle = useCallback((state: boolean) => { | |||||
| if (state) | |||||
| handleSyncWorkflowDraft(true) | |||||
| }, [handleSyncWorkflowDraft]) | |||||
| const handleGoBackToEdit = useCallback(() => { | |||||
| handleLoadBackupDraft() | |||||
| workflowStore.setState({ historyWorkflowData: undefined }) | |||||
| }, [workflowStore, handleLoadBackupDraft]) | |||||
| const handleToolConfigureUpdate = useCallback(() => { | |||||
| workflowStore.setState({ toolPublished: true }) | |||||
| }, [workflowStore]) | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3' | className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3' | ||||
| > | > | ||||
| <div> | |||||
| { | |||||
| normal && <EditingTitle /> | |||||
| } | |||||
| { | |||||
| viewHistory && <RunningTitle /> | |||||
| } | |||||
| { | |||||
| restoring && <RestoringTitle /> | |||||
| } | |||||
| </div> | |||||
| { | { | ||||
| normal && ( | normal && ( | ||||
| <div className='flex items-center gap-2'> | |||||
| {/* <GlobalVariableButton disabled={nodesReadOnly} /> */} | |||||
| {isChatMode && <ChatVariableButton disabled={nodesReadOnly} />} | |||||
| <EnvButton disabled={nodesReadOnly} /> | |||||
| <Divider type='vertical' className='mx-auto h-3.5' /> | |||||
| <RunAndHistory /> | |||||
| <Button className='text-components-button-secondary-text' onClick={handleShowFeatures}> | |||||
| <RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' /> | |||||
| {t('workflow.common.features')} | |||||
| </Button> | |||||
| <AppPublisher | |||||
| {...{ | |||||
| publishedAt, | |||||
| draftUpdatedAt, | |||||
| disabled: nodesReadOnly, | |||||
| toolPublished, | |||||
| inputs: variables, | |||||
| onRefreshData: handleToolConfigureUpdate, | |||||
| onPublish, | |||||
| onToggle: onPublisherToggle, | |||||
| crossAxisOffset: 4, | |||||
| }} | |||||
| /> | |||||
| <VersionHistoryButton onClick={onStartRestoring} /> | |||||
| </div> | |||||
| <HeaderInNormal | |||||
| {...normalProps} | |||||
| /> | |||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| viewHistory && ( | viewHistory && ( | ||||
| <div className='flex items-center space-x-2'> | |||||
| <ViewHistory withText /> | |||||
| <Divider type='vertical' className='mx-auto h-3.5' /> | |||||
| <Button | |||||
| variant='primary' | |||||
| onClick={handleGoBackToEdit} | |||||
| > | |||||
| <ArrowNarrowLeft className='mr-1 h-4 w-4' /> | |||||
| {t('workflow.common.goBackToEdit')} | |||||
| </Button> | |||||
| </div> | |||||
| <HeaderInHistory /> | |||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| restoring && ( | restoring && ( | ||||
| <div className='flex items-center justify-end gap-x-2'> | |||||
| <Button | |||||
| onClick={handleRestore} | |||||
| disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft} | |||||
| variant='primary' | |||||
| > | |||||
| {t('workflow.common.restore')} | |||||
| </Button> | |||||
| <Button | |||||
| className='text-components-button-secondary-accent-text' | |||||
| onClick={handleCancelRestore} | |||||
| > | |||||
| <div className='flex items-center gap-x-0.5'> | |||||
| <RiHistoryLine className='h-4 w-4' /> | |||||
| <span className='px-0.5'>{t('workflow.common.exitVersions')}</span> | |||||
| </div> | |||||
| </Button> | |||||
| </div> | |||||
| <HeaderInRestoring | |||||
| {...restoringProps} | |||||
| /> | |||||
| ) | ) | ||||
| } | } | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } | ||||
| export default memo(Header) | |||||
| export default Header |
| import { memo, useMemo } from 'react' | import { memo, useMemo } from 'react' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useWorkflow } from '../hooks' | |||||
| import { useFormatTimeFromNow } from '../hooks' | |||||
| import { useStore } from '../store' | import { useStore } from '../store' | ||||
| import { WorkflowVersion } from '../types' | import { WorkflowVersion } from '../types' | ||||
| import useTimestamp from '@/hooks/use-timestamp' | import useTimestamp from '@/hooks/use-timestamp' | ||||
| const RestoringTitle = () => { | const RestoringTitle = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { formatTimeFromNow } = useWorkflow() | |||||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||||
| const { formatTime } = useTimestamp() | const { formatTime } = useTimestamp() | ||||
| const currentVersion = useStore(state => state.currentVersion) | const currentVersion = useStore(state => state.currentVersion) | ||||
| const isDraft = currentVersion?.version === WorkflowVersion.Draft | const isDraft = currentVersion?.version === WorkflowVersion.Draft |
| RiErrorWarningLine, | RiErrorWarningLine, | ||||
| } from '@remixicon/react' | } from '@remixicon/react' | ||||
| import { | import { | ||||
| useFormatTimeFromNow, | |||||
| useIsChatMode, | useIsChatMode, | ||||
| useNodesInteractions, | useNodesInteractions, | ||||
| useWorkflow, | |||||
| useWorkflowInteractions, | useWorkflowInteractions, | ||||
| useWorkflowRun, | useWorkflowRun, | ||||
| } from '../hooks' | } from '../hooks' | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const isChatMode = useIsChatMode() | const isChatMode = useIsChatMode() | ||||
| const [open, setOpen] = useState(false) | const [open, setOpen] = useState(false) | ||||
| const { formatTimeFromNow } = useWorkflow() | |||||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||||
| const { | const { | ||||
| handleNodesCancelSelected, | handleNodesCancelSelected, | ||||
| } = useNodesInteractions() | } = useNodesInteractions() |
| export * from './provider' | |||||
| export * from './store' |
| import { | |||||
| createContext, | |||||
| useEffect, | |||||
| useRef, | |||||
| } from 'react' | |||||
| import { useStore } from 'reactflow' | |||||
| import { | |||||
| createHooksStore, | |||||
| } from './store' | |||||
| import type { Shape } from './store' | |||||
| type HooksStore = ReturnType<typeof createHooksStore> | |||||
| export const HooksStoreContext = createContext<HooksStore | null | undefined>(null) | |||||
| type HooksStoreContextProviderProps = Partial<Shape> & { | |||||
| children: React.ReactNode | |||||
| } | |||||
| export const HooksStoreContextProvider = ({ children, ...restProps }: HooksStoreContextProviderProps) => { | |||||
| const storeRef = useRef<HooksStore | undefined>(undefined) | |||||
| const d3Selection = useStore(s => s.d3Selection) | |||||
| const d3Zoom = useStore(s => s.d3Zoom) | |||||
| useEffect(() => { | |||||
| if (storeRef.current && d3Selection && d3Zoom) | |||||
| storeRef.current.getState().refreshAll(restProps) | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, [d3Selection, d3Zoom]) | |||||
| if (!storeRef.current) | |||||
| storeRef.current = createHooksStore(restProps) | |||||
| return ( | |||||
| <HooksStoreContext.Provider value={storeRef.current}> | |||||
| {children} | |||||
| </HooksStoreContext.Provider> | |||||
| ) | |||||
| } |
| import { useContext } from 'react' | |||||
| import { | |||||
| noop, | |||||
| } from 'lodash-es' | |||||
| import { | |||||
| useStore as useZustandStore, | |||||
| } from 'zustand' | |||||
| import { createStore } from 'zustand/vanilla' | |||||
| import { HooksStoreContext } from './provider' | |||||
| type CommonHooksFnMap = { | |||||
| doSyncWorkflowDraft: ( | |||||
| notRefreshWhenSyncError?: boolean, | |||||
| callback?: { | |||||
| onSuccess?: () => void | |||||
| onError?: () => void | |||||
| onSettled?: () => void | |||||
| } | |||||
| ) => Promise<void> | |||||
| syncWorkflowDraftWhenPageClose: () => void | |||||
| handleBackupDraft: () => void | |||||
| handleLoadBackupDraft: () => void | |||||
| handleRestoreFromPublishedWorkflow: (...args: any[]) => void | |||||
| handleRun: (...args: any[]) => void | |||||
| handleStopRun: (...args: any[]) => void | |||||
| handleStartWorkflowRun: () => void | |||||
| handleWorkflowStartRunInWorkflow: () => void | |||||
| handleWorkflowStartRunInChatflow: () => void | |||||
| } | |||||
| export type Shape = { | |||||
| refreshAll: (props: Partial<CommonHooksFnMap>) => void | |||||
| } & CommonHooksFnMap | |||||
| export const createHooksStore = ({ | |||||
| doSyncWorkflowDraft = async () => noop(), | |||||
| syncWorkflowDraftWhenPageClose = noop, | |||||
| handleBackupDraft = noop, | |||||
| handleLoadBackupDraft = noop, | |||||
| handleRestoreFromPublishedWorkflow = noop, | |||||
| handleRun = noop, | |||||
| handleStopRun = noop, | |||||
| handleStartWorkflowRun = noop, | |||||
| handleWorkflowStartRunInWorkflow = noop, | |||||
| handleWorkflowStartRunInChatflow = noop, | |||||
| }: Partial<Shape>) => { | |||||
| return createStore<Shape>(set => ({ | |||||
| refreshAll: props => set(state => ({ ...state, ...props })), | |||||
| doSyncWorkflowDraft, | |||||
| syncWorkflowDraftWhenPageClose, | |||||
| handleBackupDraft, | |||||
| handleLoadBackupDraft, | |||||
| handleRestoreFromPublishedWorkflow, | |||||
| handleRun, | |||||
| handleStopRun, | |||||
| handleStartWorkflowRun, | |||||
| handleWorkflowStartRunInWorkflow, | |||||
| handleWorkflowStartRunInChatflow, | |||||
| })) | |||||
| } | |||||
| export function useHooksStore<T>(selector: (state: Shape) => T): T { | |||||
| const store = useContext(HooksStoreContext) | |||||
| if (!store) | |||||
| throw new Error('Missing HooksStoreContext.Provider in the tree') | |||||
| return useZustandStore(store, selector) | |||||
| } | |||||
| export const useHooksStoreApi = () => { | |||||
| return useContext(HooksStoreContext)! | |||||
| } |
| export * from './use-nodes-sync-draft' | export * from './use-nodes-sync-draft' | ||||
| export * from './use-workflow' | export * from './use-workflow' | ||||
| export * from './use-workflow-run' | export * from './use-workflow-run' | ||||
| export * from './use-workflow-template' | |||||
| export * from './use-checklist' | export * from './use-checklist' | ||||
| export * from './use-selection-interactions' | export * from './use-selection-interactions' | ||||
| export * from './use-panel-interactions' | export * from './use-panel-interactions' | ||||
| export * from './use-shortcuts' | export * from './use-shortcuts' | ||||
| export * from './use-workflow-interactions' | export * from './use-workflow-interactions' | ||||
| export * from './use-workflow-mode' | export * from './use-workflow-mode' | ||||
| export * from './use-format-time-from-now' |
| import { useCallback } from 'react' | |||||
| import produce from 'immer' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| export const useEdgesInteractionsWithoutSync = () => { | |||||
| const store = useStoreApi() | |||||
| const handleEdgeCancelRunningStatus = useCallback(() => { | |||||
| const { | |||||
| edges, | |||||
| setEdges, | |||||
| } = store.getState() | |||||
| const newEdges = produce(edges, (draft) => { | |||||
| draft.forEach((edge) => { | |||||
| edge.data._sourceRunningStatus = undefined | |||||
| edge.data._targetRunningStatus = undefined | |||||
| edge.data._waitingRun = false | |||||
| }) | |||||
| }) | |||||
| setEdges(newEdges) | |||||
| }, [store]) | |||||
| return { | |||||
| handleEdgeCancelRunningStatus, | |||||
| } | |||||
| } |
| setEdges(newEdges) | setEdges(newEdges) | ||||
| }, [store, getNodesReadOnly]) | }, [store, getNodesReadOnly]) | ||||
| const handleEdgeCancelRunningStatus = useCallback(() => { | |||||
| const { | |||||
| edges, | |||||
| setEdges, | |||||
| } = store.getState() | |||||
| const newEdges = produce(edges, (draft) => { | |||||
| draft.forEach((edge) => { | |||||
| edge.data._sourceRunningStatus = undefined | |||||
| edge.data._targetRunningStatus = undefined | |||||
| edge.data._waitingRun = false | |||||
| }) | |||||
| }) | |||||
| setEdges(newEdges) | |||||
| }, [store]) | |||||
| return { | return { | ||||
| handleEdgeEnter, | handleEdgeEnter, | ||||
| handleEdgeLeave, | handleEdgeLeave, | ||||
| handleEdgeDeleteByDeleteBranch, | handleEdgeDeleteByDeleteBranch, | ||||
| handleEdgeDelete, | handleEdgeDelete, | ||||
| handleEdgesChange, | handleEdgesChange, | ||||
| handleEdgeCancelRunningStatus, | |||||
| } | } | ||||
| } | } |
| import dayjs from 'dayjs' | |||||
| import { useCallback } from 'react' | |||||
| import { useI18N } from '@/context/i18n' | |||||
| export const useFormatTimeFromNow = () => { | |||||
| const { locale } = useI18N() | |||||
| const formatTimeFromNow = useCallback((time: number) => { | |||||
| return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() | |||||
| }, [locale]) | |||||
| return { formatTimeFromNow } | |||||
| } |
| import { useCallback } from 'react' | |||||
| import produce from 'immer' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| export const useNodesInteractionsWithoutSync = () => { | |||||
| const store = useStoreApi() | |||||
| const handleNodeCancelRunningStatus = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| setNodes, | |||||
| } = store.getState() | |||||
| const nodes = getNodes() | |||||
| const newNodes = produce(nodes, (draft) => { | |||||
| draft.forEach((node) => { | |||||
| node.data._runningStatus = undefined | |||||
| node.data._waitingRun = false | |||||
| }) | |||||
| }) | |||||
| setNodes(newNodes) | |||||
| }, [store]) | |||||
| return { | |||||
| handleNodeCancelRunningStatus, | |||||
| } | |||||
| } |
| saveStateToHistory(WorkflowHistoryEvent.NodeChange) | saveStateToHistory(WorkflowHistoryEvent.NodeChange) | ||||
| }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory]) | }, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory]) | ||||
| const handleNodeCancelRunningStatus = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| setNodes, | |||||
| } = store.getState() | |||||
| const nodes = getNodes() | |||||
| const newNodes = produce(nodes, (draft) => { | |||||
| draft.forEach((node) => { | |||||
| node.data._runningStatus = undefined | |||||
| node.data._waitingRun = false | |||||
| }) | |||||
| }) | |||||
| setNodes(newNodes) | |||||
| }, [store]) | |||||
| const handleNodesCancelSelected = useCallback(() => { | const handleNodesCancelSelected = useCallback(() => { | ||||
| const { | const { | ||||
| getNodes, | getNodes, | ||||
| handleNodeDelete, | handleNodeDelete, | ||||
| handleNodeChange, | handleNodeChange, | ||||
| handleNodeAdd, | handleNodeAdd, | ||||
| handleNodeCancelRunningStatus, | |||||
| handleNodesCancelSelected, | handleNodesCancelSelected, | ||||
| handleNodeContextMenu, | handleNodeContextMenu, | ||||
| handleNodesCopy, | handleNodesCopy, |
| import { useCallback } from 'react' | import { useCallback } from 'react' | ||||
| import produce from 'immer' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| import { useParams } from 'next/navigation' | |||||
| import { | import { | ||||
| useStore, | useStore, | ||||
| useWorkflowStore, | |||||
| } from '../store' | } from '../store' | ||||
| import { BlockEnum } from '../types' | |||||
| import { useWorkflowUpdate } from '../hooks' | |||||
| import { | import { | ||||
| useNodesReadOnly, | useNodesReadOnly, | ||||
| } from './use-workflow' | } from './use-workflow' | ||||
| import { syncWorkflowDraft } from '@/service/workflow' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { API_PREFIX } from '@/config' | |||||
| import { useHooksStore } from '@/app/components/workflow/hooks-store' | |||||
| export const useNodesSyncDraft = () => { | export const useNodesSyncDraft = () => { | ||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const { getNodesReadOnly } = useNodesReadOnly() | const { getNodesReadOnly } = useNodesReadOnly() | ||||
| const { handleRefreshWorkflowDraft } = useWorkflowUpdate() | |||||
| const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft) | const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft) | ||||
| const params = useParams() | |||||
| const getPostParams = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| edges, | |||||
| transform, | |||||
| } = store.getState() | |||||
| const [x, y, zoom] = transform | |||||
| const { | |||||
| appId, | |||||
| conversationVariables, | |||||
| environmentVariables, | |||||
| syncWorkflowDraftHash, | |||||
| } = workflowStore.getState() | |||||
| if (appId) { | |||||
| const nodes = getNodes() | |||||
| const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| if (!hasStartNode) | |||||
| return | |||||
| const features = featuresStore!.getState().features | |||||
| const producedNodes = produce(nodes, (draft) => { | |||||
| draft.forEach((node) => { | |||||
| Object.keys(node.data).forEach((key) => { | |||||
| if (key.startsWith('_')) | |||||
| delete node.data[key] | |||||
| }) | |||||
| }) | |||||
| }) | |||||
| const producedEdges = produce(edges, (draft) => { | |||||
| draft.forEach((edge) => { | |||||
| Object.keys(edge.data).forEach((key) => { | |||||
| if (key.startsWith('_')) | |||||
| delete edge.data[key] | |||||
| }) | |||||
| }) | |||||
| }) | |||||
| return { | |||||
| url: `/apps/${appId}/workflows/draft`, | |||||
| params: { | |||||
| graph: { | |||||
| nodes: producedNodes, | |||||
| edges: producedEdges, | |||||
| viewport: { | |||||
| x, | |||||
| y, | |||||
| zoom, | |||||
| }, | |||||
| }, | |||||
| features: { | |||||
| opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '', | |||||
| suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [], | |||||
| suggested_questions_after_answer: features.suggested, | |||||
| text_to_speech: features.text2speech, | |||||
| speech_to_text: features.speech2text, | |||||
| retriever_resource: features.citation, | |||||
| sensitive_word_avoidance: features.moderation, | |||||
| file_upload: features.file, | |||||
| }, | |||||
| environment_variables: environmentVariables, | |||||
| conversation_variables: conversationVariables, | |||||
| hash: syncWorkflowDraftHash, | |||||
| }, | |||||
| } | |||||
| } | |||||
| }, [store, featuresStore, workflowStore]) | |||||
| const syncWorkflowDraftWhenPageClose = useCallback(() => { | |||||
| if (getNodesReadOnly()) | |||||
| return | |||||
| const postParams = getPostParams() | |||||
| if (postParams) { | |||||
| navigator.sendBeacon( | |||||
| `${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`, | |||||
| JSON.stringify(postParams.params), | |||||
| ) | |||||
| } | |||||
| }, [getPostParams, params.appId, getNodesReadOnly]) | |||||
| const doSyncWorkflowDraft = useCallback(async ( | |||||
| notRefreshWhenSyncError?: boolean, | |||||
| callback?: { | |||||
| onSuccess?: () => void | |||||
| onError?: () => void | |||||
| onSettled?: () => void | |||||
| }, | |||||
| ) => { | |||||
| if (getNodesReadOnly()) | |||||
| return | |||||
| const postParams = getPostParams() | |||||
| if (postParams) { | |||||
| const { | |||||
| setSyncWorkflowDraftHash, | |||||
| setDraftUpdatedAt, | |||||
| } = workflowStore.getState() | |||||
| try { | |||||
| const res = await syncWorkflowDraft(postParams) | |||||
| setSyncWorkflowDraftHash(res.hash) | |||||
| setDraftUpdatedAt(res.updated_at) | |||||
| callback?.onSuccess && callback.onSuccess() | |||||
| } | |||||
| catch (error: any) { | |||||
| if (error && error.json && !error.bodyUsed) { | |||||
| error.json().then((err: any) => { | |||||
| if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError) | |||||
| handleRefreshWorkflowDraft() | |||||
| }) | |||||
| } | |||||
| callback?.onError && callback.onError() | |||||
| } | |||||
| finally { | |||||
| callback?.onSettled && callback.onSettled() | |||||
| } | |||||
| } | |||||
| }, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft]) | |||||
| const doSyncWorkflowDraft = useHooksStore(s => s.doSyncWorkflowDraft) | |||||
| const syncWorkflowDraftWhenPageClose = useHooksStore(s => s.syncWorkflowDraftWhenPageClose) | |||||
| const handleSyncWorkflowDraft = useCallback(( | const handleSyncWorkflowDraft = useCallback(( | ||||
| sync?: boolean, | sync?: boolean, |
| useSelectionInteractions, | useSelectionInteractions, | ||||
| useWorkflowReadOnly, | useWorkflowReadOnly, | ||||
| } from '../hooks' | } from '../hooks' | ||||
| import { useEdgesInteractions } from './use-edges-interactions' | |||||
| import { useNodesInteractions } from './use-nodes-interactions' | |||||
| import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync' | |||||
| import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync' | |||||
| import { useNodesSyncDraft } from './use-nodes-sync-draft' | import { useNodesSyncDraft } from './use-nodes-sync-draft' | ||||
| import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | ||||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | import { useEventEmitterContextContext } from '@/context/event-emitter' | ||||
| export const useWorkflowInteractions = () => { | export const useWorkflowInteractions = () => { | ||||
| const workflowStore = useWorkflowStore() | const workflowStore = useWorkflowStore() | ||||
| const { handleNodeCancelRunningStatus } = useNodesInteractions() | |||||
| const { handleEdgeCancelRunningStatus } = useEdgesInteractions() | |||||
| const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync() | |||||
| const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() | |||||
| const handleCancelDebugAndPreviewPanel = useCallback(() => { | const handleCancelDebugAndPreviewPanel = useCallback(() => { | ||||
| workflowStore.setState({ | workflowStore.setState({ |
| import { useCallback } from 'react' | |||||
| import { | |||||
| useReactFlow, | |||||
| useStoreApi, | |||||
| } from 'reactflow' | |||||
| import produce from 'immer' | |||||
| import { v4 as uuidV4 } from 'uuid' | |||||
| import { usePathname } from 'next/navigation' | |||||
| import { useWorkflowStore } from '../store' | |||||
| import { useNodesSyncDraft } from '../hooks' | |||||
| import { WorkflowRunningStatus } from '../types' | |||||
| import { useWorkflowUpdate } from './use-workflow-interactions' | |||||
| import { useWorkflowRunEvent } from './use-workflow-run-event/use-workflow-run-event' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import type { IOtherOptions } from '@/service/base' | |||||
| import { ssePost } from '@/service/base' | |||||
| import { stopWorkflowRun } from '@/service/workflow' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager' | |||||
| import type { VersionHistory } from '@/types/workflow' | |||||
| import { noop } from 'lodash-es' | |||||
| import { useHooksStore } from '@/app/components/workflow/hooks-store' | |||||
| export const useWorkflowRun = () => { | export const useWorkflowRun = () => { | ||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const reactflow = useReactFlow() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const { doSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const { handleUpdateWorkflowCanvas } = useWorkflowUpdate() | |||||
| const pathname = usePathname() | |||||
| const { | |||||
| handleWorkflowStarted, | |||||
| handleWorkflowFinished, | |||||
| handleWorkflowFailed, | |||||
| handleWorkflowNodeStarted, | |||||
| handleWorkflowNodeFinished, | |||||
| handleWorkflowNodeIterationStarted, | |||||
| handleWorkflowNodeIterationNext, | |||||
| handleWorkflowNodeIterationFinished, | |||||
| handleWorkflowNodeLoopStarted, | |||||
| handleWorkflowNodeLoopNext, | |||||
| handleWorkflowNodeLoopFinished, | |||||
| handleWorkflowNodeRetry, | |||||
| handleWorkflowAgentLog, | |||||
| handleWorkflowTextChunk, | |||||
| handleWorkflowTextReplace, | |||||
| } = useWorkflowRunEvent() | |||||
| const handleBackupDraft = useCallback(() => { | |||||
| const { | |||||
| getNodes, | |||||
| edges, | |||||
| } = store.getState() | |||||
| const { getViewport } = reactflow | |||||
| const { | |||||
| backupDraft, | |||||
| setBackupDraft, | |||||
| environmentVariables, | |||||
| } = workflowStore.getState() | |||||
| const { features } = featuresStore!.getState() | |||||
| if (!backupDraft) { | |||||
| setBackupDraft({ | |||||
| nodes: getNodes(), | |||||
| edges, | |||||
| viewport: getViewport(), | |||||
| features, | |||||
| environmentVariables, | |||||
| }) | |||||
| doSyncWorkflowDraft() | |||||
| } | |||||
| }, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft]) | |||||
| const handleLoadBackupDraft = useCallback(() => { | |||||
| const { | |||||
| backupDraft, | |||||
| setBackupDraft, | |||||
| setEnvironmentVariables, | |||||
| } = workflowStore.getState() | |||||
| if (backupDraft) { | |||||
| const { | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| features, | |||||
| environmentVariables, | |||||
| } = backupDraft | |||||
| handleUpdateWorkflowCanvas({ | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| }) | |||||
| setEnvironmentVariables(environmentVariables) | |||||
| featuresStore!.setState({ features }) | |||||
| setBackupDraft(undefined) | |||||
| } | |||||
| }, [handleUpdateWorkflowCanvas, workflowStore, featuresStore]) | |||||
| 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 | |||||
| node.data._runningStatus = undefined | |||||
| }) | |||||
| }) | |||||
| setNodes(newNodes) | |||||
| await doSyncWorkflowDraft() | |||||
| const { | |||||
| onWorkflowStarted, | |||||
| onWorkflowFinished, | |||||
| onNodeStarted, | |||||
| onNodeFinished, | |||||
| onIterationStart, | |||||
| onIterationNext, | |||||
| onIterationFinish, | |||||
| onLoopStart, | |||||
| onLoopNext, | |||||
| onLoopFinish, | |||||
| onNodeRetry, | |||||
| onAgentLog, | |||||
| onError, | |||||
| ...restCallback | |||||
| } = callback || {} | |||||
| workflowStore.setState({ historyWorkflowData: undefined }) | |||||
| const appDetail = useAppStore.getState().appDetail | |||||
| const workflowContainer = document.getElementById('workflow-container') | |||||
| const { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| } = workflowContainer! | |||||
| let url = '' | |||||
| if (appDetail?.mode === 'advanced-chat') | |||||
| url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run` | |||||
| if (appDetail?.mode === 'workflow') | |||||
| url = `/apps/${appDetail.id}/workflows/draft/run` | |||||
| const { | |||||
| setWorkflowRunningData, | |||||
| } = workflowStore.getState() | |||||
| setWorkflowRunningData({ | |||||
| result: { | |||||
| status: WorkflowRunningStatus.Running, | |||||
| }, | |||||
| tracing: [], | |||||
| resultText: '', | |||||
| }) | |||||
| let ttsUrl = '' | |||||
| let ttsIsPublic = false | |||||
| if (params.token) { | |||||
| ttsUrl = '/text-to-audio' | |||||
| ttsIsPublic = true | |||||
| } | |||||
| else if (params.appId) { | |||||
| if (pathname.search('explore/installed') > -1) | |||||
| ttsUrl = `/installed-apps/${params.appId}/text-to-audio` | |||||
| else | |||||
| ttsUrl = `/apps/${params.appId}/text-to-audio` | |||||
| } | |||||
| const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', noop) | |||||
| ssePost( | |||||
| url, | |||||
| { | |||||
| body: params, | |||||
| }, | |||||
| { | |||||
| onWorkflowStarted: (params) => { | |||||
| handleWorkflowStarted(params) | |||||
| if (onWorkflowStarted) | |||||
| onWorkflowStarted(params) | |||||
| }, | |||||
| onWorkflowFinished: (params) => { | |||||
| handleWorkflowFinished(params) | |||||
| if (onWorkflowFinished) | |||||
| onWorkflowFinished(params) | |||||
| }, | |||||
| onError: (params) => { | |||||
| handleWorkflowFailed() | |||||
| if (onError) | |||||
| onError(params) | |||||
| }, | |||||
| onNodeStarted: (params) => { | |||||
| handleWorkflowNodeStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onNodeStarted) | |||||
| onNodeStarted(params) | |||||
| }, | |||||
| onNodeFinished: (params) => { | |||||
| handleWorkflowNodeFinished(params) | |||||
| if (onNodeFinished) | |||||
| onNodeFinished(params) | |||||
| }, | |||||
| onIterationStart: (params) => { | |||||
| handleWorkflowNodeIterationStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onIterationStart) | |||||
| onIterationStart(params) | |||||
| }, | |||||
| onIterationNext: (params) => { | |||||
| handleWorkflowNodeIterationNext(params) | |||||
| if (onIterationNext) | |||||
| onIterationNext(params) | |||||
| }, | |||||
| onIterationFinish: (params) => { | |||||
| handleWorkflowNodeIterationFinished(params) | |||||
| if (onIterationFinish) | |||||
| onIterationFinish(params) | |||||
| }, | |||||
| onLoopStart: (params) => { | |||||
| handleWorkflowNodeLoopStarted( | |||||
| params, | |||||
| { | |||||
| clientWidth, | |||||
| clientHeight, | |||||
| }, | |||||
| ) | |||||
| if (onLoopStart) | |||||
| onLoopStart(params) | |||||
| }, | |||||
| onLoopNext: (params) => { | |||||
| handleWorkflowNodeLoopNext(params) | |||||
| if (onLoopNext) | |||||
| onLoopNext(params) | |||||
| }, | |||||
| onLoopFinish: (params) => { | |||||
| handleWorkflowNodeLoopFinished(params) | |||||
| if (onLoopFinish) | |||||
| onLoopFinish(params) | |||||
| }, | |||||
| onNodeRetry: (params) => { | |||||
| handleWorkflowNodeRetry(params) | |||||
| if (onNodeRetry) | |||||
| onNodeRetry(params) | |||||
| }, | |||||
| onAgentLog: (params) => { | |||||
| handleWorkflowAgentLog(params) | |||||
| if (onAgentLog) | |||||
| onAgentLog(params) | |||||
| }, | |||||
| onTextChunk: (params) => { | |||||
| handleWorkflowTextChunk(params) | |||||
| }, | |||||
| onTextReplace: (params) => { | |||||
| handleWorkflowTextReplace(params) | |||||
| }, | |||||
| onTTSChunk: (messageId: string, audio: string) => { | |||||
| if (!audio || audio === '') | |||||
| return | |||||
| player.playAudioWithAudio(audio, true) | |||||
| AudioPlayerManager.getInstance().resetMsgId(messageId) | |||||
| }, | |||||
| onTTSEnd: (messageId: string, audio: string) => { | |||||
| player.playAudioWithAudio(audio, false) | |||||
| }, | |||||
| ...restCallback, | |||||
| }, | |||||
| ) | |||||
| }, [ | |||||
| store, | |||||
| workflowStore, | |||||
| doSyncWorkflowDraft, | |||||
| handleWorkflowStarted, | |||||
| handleWorkflowFinished, | |||||
| handleWorkflowFailed, | |||||
| handleWorkflowNodeStarted, | |||||
| handleWorkflowNodeFinished, | |||||
| handleWorkflowNodeIterationStarted, | |||||
| handleWorkflowNodeIterationNext, | |||||
| handleWorkflowNodeIterationFinished, | |||||
| handleWorkflowNodeLoopStarted, | |||||
| handleWorkflowNodeLoopNext, | |||||
| handleWorkflowNodeLoopFinished, | |||||
| handleWorkflowNodeRetry, | |||||
| handleWorkflowTextChunk, | |||||
| handleWorkflowTextReplace, | |||||
| handleWorkflowAgentLog, | |||||
| pathname], | |||||
| ) | |||||
| const handleStopRun = useCallback((taskId: string) => { | |||||
| const appId = useAppStore.getState().appDetail?.id | |||||
| stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`) | |||||
| }, []) | |||||
| const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => { | |||||
| const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } })) | |||||
| const edges = publishedWorkflow.graph.edges | |||||
| const viewport = publishedWorkflow.graph.viewport! | |||||
| handleUpdateWorkflowCanvas({ | |||||
| nodes, | |||||
| edges, | |||||
| viewport, | |||||
| }) | |||||
| const mappedFeatures = { | |||||
| opening: { | |||||
| enabled: !!publishedWorkflow.features.opening_statement || !!publishedWorkflow.features.suggested_questions.length, | |||||
| opening_statement: publishedWorkflow.features.opening_statement, | |||||
| suggested_questions: publishedWorkflow.features.suggested_questions, | |||||
| }, | |||||
| suggested: publishedWorkflow.features.suggested_questions_after_answer, | |||||
| text2speech: publishedWorkflow.features.text_to_speech, | |||||
| speech2text: publishedWorkflow.features.speech_to_text, | |||||
| citation: publishedWorkflow.features.retriever_resource, | |||||
| moderation: publishedWorkflow.features.sensitive_word_avoidance, | |||||
| file: publishedWorkflow.features.file_upload, | |||||
| } | |||||
| featuresStore?.setState({ features: mappedFeatures }) | |||||
| workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || []) | |||||
| }, [featuresStore, handleUpdateWorkflowCanvas, workflowStore]) | |||||
| const handleBackupDraft = useHooksStore(s => s.handleBackupDraft) | |||||
| const handleLoadBackupDraft = useHooksStore(s => s.handleLoadBackupDraft) | |||||
| const handleRestoreFromPublishedWorkflow = useHooksStore(s => s.handleRestoreFromPublishedWorkflow) | |||||
| const handleRun = useHooksStore(s => s.handleRun) | |||||
| const handleStopRun = useHooksStore(s => s.handleStopRun) | |||||
| return { | return { | ||||
| handleBackupDraft, | handleBackupDraft, |
| import { useCallback } from 'react' | |||||
| import { useStoreApi } from 'reactflow' | |||||
| import { useWorkflowStore } from '../store' | |||||
| import { | |||||
| BlockEnum, | |||||
| WorkflowRunningStatus, | |||||
| } from '../types' | |||||
| import { | |||||
| useIsChatMode, | |||||
| useNodesSyncDraft, | |||||
| useWorkflowInteractions, | |||||
| useWorkflowRun, | |||||
| } from './index' | |||||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||||
| import { useHooksStore } from '@/app/components/workflow/hooks-store' | |||||
| export const useWorkflowStartRun = () => { | export const useWorkflowStartRun = () => { | ||||
| const store = useStoreApi() | |||||
| const workflowStore = useWorkflowStore() | |||||
| const featuresStore = useFeaturesStore() | |||||
| const isChatMode = useIsChatMode() | |||||
| const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions() | |||||
| const { handleRun } = useWorkflowRun() | |||||
| const { doSyncWorkflowDraft } = useNodesSyncDraft() | |||||
| const handleWorkflowStartRunInWorkflow = useCallback(async () => { | |||||
| const { | |||||
| workflowRunningData, | |||||
| } = workflowStore.getState() | |||||
| if (workflowRunningData?.result.status === WorkflowRunningStatus.Running) | |||||
| return | |||||
| const { getNodes } = store.getState() | |||||
| const nodes = getNodes() | |||||
| const startNode = nodes.find(node => node.data.type === BlockEnum.Start) | |||||
| const startVariables = startNode?.data.variables || [] | |||||
| const fileSettings = featuresStore!.getState().features.file | |||||
| const { | |||||
| showDebugAndPreviewPanel, | |||||
| setShowDebugAndPreviewPanel, | |||||
| setShowInputsPanel, | |||||
| setShowEnvPanel, | |||||
| } = workflowStore.getState() | |||||
| setShowEnvPanel(false) | |||||
| if (showDebugAndPreviewPanel) { | |||||
| handleCancelDebugAndPreviewPanel() | |||||
| return | |||||
| } | |||||
| if (!startVariables.length && !fileSettings?.image?.enabled) { | |||||
| await doSyncWorkflowDraft() | |||||
| handleRun({ inputs: {}, files: [] }) | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setShowInputsPanel(false) | |||||
| } | |||||
| else { | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setShowInputsPanel(true) | |||||
| } | |||||
| }, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft]) | |||||
| const handleWorkflowStartRunInChatflow = useCallback(async () => { | |||||
| const { | |||||
| showDebugAndPreviewPanel, | |||||
| setShowDebugAndPreviewPanel, | |||||
| setHistoryWorkflowData, | |||||
| setShowEnvPanel, | |||||
| setShowChatVariablePanel, | |||||
| } = workflowStore.getState() | |||||
| setShowEnvPanel(false) | |||||
| setShowChatVariablePanel(false) | |||||
| if (showDebugAndPreviewPanel) | |||||
| handleCancelDebugAndPreviewPanel() | |||||
| else | |||||
| setShowDebugAndPreviewPanel(true) | |||||
| setHistoryWorkflowData(undefined) | |||||
| }, [workflowStore, handleCancelDebugAndPreviewPanel]) | |||||
| const handleStartWorkflowRun = useCallback(() => { | |||||
| if (!isChatMode) | |||||
| handleWorkflowStartRunInWorkflow() | |||||
| else | |||||
| handleWorkflowStartRunInChatflow() | |||||
| }, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow]) | |||||
| const handleStartWorkflowRun = useHooksStore(s => s.handleStartWorkflowRun) | |||||
| const handleWorkflowStartRunInWorkflow = useHooksStore(s => s.handleWorkflowStartRunInWorkflow) | |||||
| const handleWorkflowStartRunInChatflow = useHooksStore(s => s.handleWorkflowStartRunInChatflow) | |||||
| return { | return { | ||||
| handleStartWorkflowRun, | handleStartWorkflowRun, |
| import { | import { | ||||
| useCallback, | useCallback, | ||||
| useEffect, | |||||
| useMemo, | useMemo, | ||||
| useState, | |||||
| } from 'react' | } from 'react' | ||||
| import dayjs from 'dayjs' | |||||
| import { uniqBy } from 'lodash-es' | import { uniqBy } from 'lodash-es' | ||||
| import { useTranslation } from 'react-i18next' | import { useTranslation } from 'react-i18next' | ||||
| import { useContext } from 'use-context-selector' | |||||
| import { | import { | ||||
| getIncomers, | getIncomers, | ||||
| getOutgoers, | getOutgoers, | ||||
| import { CUSTOM_NOTE_NODE } from '../note-node/constants' | import { CUSTOM_NOTE_NODE } from '../note-node/constants' | ||||
| import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | ||||
| import { useNodesExtraData } from './use-nodes-data' | import { useNodesExtraData } from './use-nodes-data' | ||||
| import { useWorkflowTemplate } from './use-workflow-template' | |||||
| import { useStore as useAppStore } from '@/app/components/app/store' | import { useStore as useAppStore } from '@/app/components/app/store' | ||||
| import { | |||||
| fetchNodesDefaultConfigs, | |||||
| fetchPublishedWorkflow, | |||||
| fetchWorkflowDraft, | |||||
| syncWorkflowDraft, | |||||
| } from '@/service/workflow' | |||||
| import type { FetchWorkflowDraftResponse } from '@/types/workflow' | |||||
| import { | import { | ||||
| fetchAllBuiltInTools, | fetchAllBuiltInTools, | ||||
| fetchAllCustomTools, | fetchAllCustomTools, | ||||
| fetchAllWorkflowTools, | fetchAllWorkflowTools, | ||||
| } from '@/service/tools' | } from '@/service/tools' | ||||
| import I18n from '@/context/i18n' | |||||
| import { CollectionType } from '@/app/components/tools/types' | import { CollectionType } from '@/app/components/tools/types' | ||||
| import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants' | 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_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants' | ||||
| import { useWorkflowConfig } from '@/service/use-workflow' | |||||
| import { basePath } from '@/utils/var' | import { basePath } from '@/utils/var' | ||||
| import { canFindTool } from '@/utils' | import { canFindTool } from '@/utils' | ||||
| export const useWorkflow = () => { | export const useWorkflow = () => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { locale } = useContext(I18n) | |||||
| const store = useStoreApi() | const store = useStoreApi() | ||||
| const workflowStore = useWorkflowStore() | const workflowStore = useWorkflowStore() | ||||
| const appId = useStore(s => s.appId) | |||||
| const nodesExtraData = useNodesExtraData() | const nodesExtraData = useNodesExtraData() | ||||
| const { data: workflowConfig } = useWorkflowConfig(appId) | |||||
| const setPanelWidth = useCallback((width: number) => { | const setPanelWidth = useCallback((width: number) => { | ||||
| localStorage.setItem('workflow-node-panel-width', `${width}`) | localStorage.setItem('workflow-node-panel-width', `${width}`) | ||||
| workflowStore.setState({ panelWidth: width }) | workflowStore.setState({ panelWidth: width }) | ||||
| list.push(...incomers) | list.push(...incomers) | ||||
| return uniqBy(list, 'id').filter((item) => { | |||||
| return uniqBy(list, 'id').filter((item: Node) => { | |||||
| return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type) | return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type) | ||||
| }) | }) | ||||
| }, [store]) | }, [store]) | ||||
| const length = list.length | const length = list.length | ||||
| if (length) { | if (length) { | ||||
| return uniqBy(list, 'id').reverse().filter((item) => { | |||||
| return uniqBy(list, 'id').reverse().filter((item: Node) => { | |||||
| return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type) | return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type) | ||||
| }) | }) | ||||
| } | } | ||||
| parallelList, | parallelList, | ||||
| hasAbnormalEdges, | hasAbnormalEdges, | ||||
| } = getParallelInfo(nodes, edges, parentNodeId) | } = getParallelInfo(nodes, edges, parentNodeId) | ||||
| const { workflowConfig } = workflowStore.getState() | |||||
| if (hasAbnormalEdges) | if (hasAbnormalEdges) | ||||
| return false | return false | ||||
| } | } | ||||
| return true | return true | ||||
| }, [t, workflowStore, workflowConfig?.parallel_depth_limit]) | |||||
| }, [t, workflowStore]) | |||||
| const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => { | const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => { | ||||
| const { | const { | ||||
| return !hasCycle(targetNode) | return !hasCycle(targetNode) | ||||
| }, [store, nodesExtraData, checkParallelLimit]) | }, [store, nodesExtraData, checkParallelLimit]) | ||||
| const formatTimeFromNow = useCallback((time: number) => { | |||||
| return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow() | |||||
| }, [locale]) | |||||
| const getNode = useCallback((nodeId?: string) => { | const getNode = useCallback((nodeId?: string) => { | ||||
| const { getNodes } = store.getState() | const { getNodes } = store.getState() | ||||
| const nodes = getNodes() | const nodes = getNodes() | ||||
| checkNestedParallelLimit, | checkNestedParallelLimit, | ||||
| isValidConnection, | isValidConnection, | ||||
| isFromStartNode, | isFromStartNode, | ||||
| formatTimeFromNow, | |||||
| getNode, | getNode, | ||||
| getBeforeNodeById, | getBeforeNodeById, | ||||
| getIterationNodeChildren, | getIterationNodeChildren, | ||||
| } | } | ||||
| } | } | ||||
| export const useWorkflowInit = () => { | |||||
| const workflowStore = useWorkflowStore() | |||||
| const { | |||||
| nodes: nodesTemplate, | |||||
| edges: edgesTemplate, | |||||
| } = useWorkflowTemplate() | |||||
| const { handleFetchAllTools } = useFetchToolsData() | |||||
| const appDetail = useAppStore(state => state.appDetail)! | |||||
| const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash) | |||||
| const [data, setData] = useState<FetchWorkflowDraftResponse>() | |||||
| const [isLoading, setIsLoading] = useState(true) | |||||
| useEffect(() => { | |||||
| workflowStore.setState({ appId: appDetail.id }) | |||||
| }, [appDetail.id, workflowStore]) | |||||
| const handleGetInitialWorkflowData = useCallback(async () => { | |||||
| try { | |||||
| const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`) | |||||
| setData(res) | |||||
| workflowStore.setState({ | |||||
| envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => { | |||||
| acc[env.id] = env.value | |||||
| return acc | |||||
| }, {} as Record<string, string>), | |||||
| environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [], | |||||
| conversationVariables: res.conversation_variables || [], | |||||
| }) | |||||
| setSyncWorkflowDraftHash(res.hash) | |||||
| setIsLoading(false) | |||||
| } | |||||
| catch (error: any) { | |||||
| if (error && error.json && !error.bodyUsed && appDetail) { | |||||
| error.json().then((err: any) => { | |||||
| if (err.code === 'draft_workflow_not_exist') { | |||||
| workflowStore.setState({ notInitialWorkflow: true }) | |||||
| syncWorkflowDraft({ | |||||
| url: `/apps/${appDetail.id}/workflows/draft`, | |||||
| params: { | |||||
| graph: { | |||||
| nodes: nodesTemplate, | |||||
| edges: edgesTemplate, | |||||
| }, | |||||
| features: { | |||||
| retriever_resource: { enabled: true }, | |||||
| }, | |||||
| environment_variables: [], | |||||
| conversation_variables: [], | |||||
| }, | |||||
| }).then((res) => { | |||||
| workflowStore.getState().setDraftUpdatedAt(res.updated_at) | |||||
| handleGetInitialWorkflowData() | |||||
| }) | |||||
| } | |||||
| }) | |||||
| } | |||||
| } | |||||
| }, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash]) | |||||
| useEffect(() => { | |||||
| handleGetInitialWorkflowData() | |||||
| // eslint-disable-next-line react-hooks/exhaustive-deps | |||||
| }, []) | |||||
| const handleFetchPreloadData = useCallback(async () => { | |||||
| try { | |||||
| const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`) | |||||
| const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`) | |||||
| workflowStore.setState({ | |||||
| nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => { | |||||
| if (!acc[block.type]) | |||||
| acc[block.type] = { ...block.config } | |||||
| return acc | |||||
| }, {} as Record<string, any>), | |||||
| }) | |||||
| workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at) | |||||
| } | |||||
| catch (e) { | |||||
| console.error(e) | |||||
| } | |||||
| }, [workflowStore, appDetail]) | |||||
| useEffect(() => { | |||||
| handleFetchPreloadData() | |||||
| handleFetchAllTools('builtin') | |||||
| handleFetchAllTools('custom') | |||||
| handleFetchAllTools('workflow') | |||||
| }, [handleFetchPreloadData, handleFetchAllTools]) | |||||
| useEffect(() => { | |||||
| if (data) { | |||||
| workflowStore.getState().setDraftUpdatedAt(data.updated_at) | |||||
| workflowStore.getState().setToolPublished(data.tool_published) | |||||
| } | |||||
| }, [data, workflowStore]) | |||||
| return { | |||||
| data, | |||||
| isLoading, | |||||
| } | |||||
| } | |||||
| export const useWorkflowReadOnly = () => { | export const useWorkflowReadOnly = () => { | ||||
| const workflowStore = useWorkflowStore() | const workflowStore = useWorkflowStore() | ||||
| const workflowRunningData = useStore(s => s.workflowRunningData) | const workflowRunningData = useStore(s => s.workflowRunningData) |
| memo, | memo, | ||||
| useCallback, | useCallback, | ||||
| useEffect, | useEffect, | ||||
| useMemo, | |||||
| useRef, | useRef, | ||||
| useState, | |||||
| } from 'react' | } from 'react' | ||||
| import useSWR from 'swr' | |||||
| import { setAutoFreeze } from 'immer' | import { setAutoFreeze } from 'immer' | ||||
| import { | import { | ||||
| useEventListener, | useEventListener, | ||||
| import './style.css' | import './style.css' | ||||
| import type { | import type { | ||||
| Edge, | Edge, | ||||
| EnvironmentVariable, | |||||
| Node, | Node, | ||||
| } from './types' | } from './types' | ||||
| import { | import { | ||||
| ControlMode, | ControlMode, | ||||
| SupportUploadFileTypes, | |||||
| } from './types' | } from './types' | ||||
| import { WorkflowContextProvider } from './context' | |||||
| import { | import { | ||||
| useDSL, | |||||
| useEdgesInteractions, | useEdgesInteractions, | ||||
| useFetchToolsData, | |||||
| useNodesInteractions, | useNodesInteractions, | ||||
| useNodesReadOnly, | useNodesReadOnly, | ||||
| useNodesSyncDraft, | useNodesSyncDraft, | ||||
| useSelectionInteractions, | useSelectionInteractions, | ||||
| useShortcuts, | useShortcuts, | ||||
| useWorkflow, | useWorkflow, | ||||
| useWorkflowInit, | |||||
| useWorkflowReadOnly, | useWorkflowReadOnly, | ||||
| useWorkflowUpdate, | useWorkflowUpdate, | ||||
| } from './hooks' | } from './hooks' | ||||
| import Header from './header' | |||||
| import CustomNode from './nodes' | import CustomNode from './nodes' | ||||
| import CustomNoteNode from './note-node' | import CustomNoteNode from './note-node' | ||||
| import { CUSTOM_NOTE_NODE } from './note-node/constants' | import { CUSTOM_NOTE_NODE } from './note-node/constants' | ||||
| import Operator from './operator' | import Operator from './operator' | ||||
| import CustomEdge from './custom-edge' | import CustomEdge from './custom-edge' | ||||
| import CustomConnectionLine from './custom-connection-line' | import CustomConnectionLine from './custom-connection-line' | ||||
| import Panel from './panel' | |||||
| import Features from './features' | |||||
| import HelpLine from './help-line' | import HelpLine from './help-line' | ||||
| import CandidateNode from './candidate-node' | import CandidateNode from './candidate-node' | ||||
| import PanelContextmenu from './panel-contextmenu' | import PanelContextmenu from './panel-contextmenu' | ||||
| import NodeContextmenu from './node-contextmenu' | import NodeContextmenu from './node-contextmenu' | ||||
| import SyncingDataModal from './syncing-data-modal' | 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 LimitTips from './limit-tips' | ||||
| import PluginDependency from './plugin-dependency' | |||||
| import { | import { | ||||
| useStore, | useStore, | ||||
| useWorkflowStore, | useWorkflowStore, | ||||
| } from './store' | } from './store' | ||||
| import { | |||||
| initialEdges, | |||||
| initialNodes, | |||||
| } from './utils' | |||||
| import { | import { | ||||
| CUSTOM_EDGE, | CUSTOM_EDGE, | ||||
| CUSTOM_NODE, | CUSTOM_NODE, | ||||
| DSL_EXPORT_CHECK, | |||||
| ITERATION_CHILDREN_Z_INDEX, | ITERATION_CHILDREN_Z_INDEX, | ||||
| WORKFLOW_DATA_UPDATE, | WORKFLOW_DATA_UPDATE, | ||||
| } from './constants' | } from './constants' | ||||
| import { WorkflowHistoryProvider } from './workflow-history-store' | 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 { useEventEmitterContextContext } from '@/context/event-emitter' | ||||
| import Confirm from '@/app/components/base/confirm' | import Confirm from '@/app/components/base/confirm' | ||||
| import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' | |||||
| import { fetchFileUploadConfig } from '@/service/common' | |||||
| import DatasetsDetailProvider from './datasets-detail-store/provider' | import DatasetsDetailProvider from './datasets-detail-store/provider' | ||||
| import { HooksStoreContextProvider } from './hooks-store' | |||||
| import type { Shape as HooksStoreShape } from './hooks-store' | |||||
| const nodeTypes = { | const nodeTypes = { | ||||
| [CUSTOM_NODE]: CustomNode, | [CUSTOM_NODE]: CustomNode, | ||||
| [CUSTOM_EDGE]: CustomEdge, | [CUSTOM_EDGE]: CustomEdge, | ||||
| } | } | ||||
| type WorkflowProps = { | |||||
| export type WorkflowProps = { | |||||
| nodes: Node[] | nodes: Node[] | ||||
| edges: Edge[] | edges: Edge[] | ||||
| viewport?: Viewport | viewport?: Viewport | ||||
| children?: React.ReactNode | |||||
| onWorkflowDataUpdate?: (v: any) => void | |||||
| } | } | ||||
| const Workflow: FC<WorkflowProps> = memo(({ | |||||
| export const Workflow: FC<WorkflowProps> = memo(({ | |||||
| nodes: originalNodes, | nodes: originalNodes, | ||||
| edges: originalEdges, | edges: originalEdges, | ||||
| viewport, | viewport, | ||||
| children, | |||||
| onWorkflowDataUpdate, | |||||
| }) => { | }) => { | ||||
| const workflowContainerRef = useRef<HTMLDivElement>(null) | const workflowContainerRef = useRef<HTMLDivElement>(null) | ||||
| const workflowStore = useWorkflowStore() | const workflowStore = useWorkflowStore() | ||||
| const reactflow = useReactFlow() | const reactflow = useReactFlow() | ||||
| const featuresStore = useFeaturesStore() | |||||
| const [nodes, setNodes] = useNodesState(originalNodes) | const [nodes, setNodes] = useNodesState(originalNodes) | ||||
| const [edges, setEdges] = useEdgesState(originalEdges) | const [edges, setEdges] = useEdgesState(originalEdges) | ||||
| const showFeaturesPanel = useStore(state => state.showFeaturesPanel) | |||||
| const controlMode = useStore(s => s.controlMode) | const controlMode = useStore(s => s.controlMode) | ||||
| const nodeAnimation = useStore(s => s.nodeAnimation) | const nodeAnimation = useStore(s => s.nodeAnimation) | ||||
| const showConfirm = useStore(s => s.showConfirm) | const showConfirm = useStore(s => s.showConfirm) | ||||
| const showImportDSLModal = useStore(s => s.showImportDSLModal) | |||||
| const { | const { | ||||
| setShowConfirm, | setShowConfirm, | ||||
| setControlPromptEditorRerenderKey, | setControlPromptEditorRerenderKey, | ||||
| setShowImportDSLModal, | |||||
| setSyncWorkflowDraftHash, | setSyncWorkflowDraftHash, | ||||
| } = workflowStore.getState() | } = workflowStore.getState() | ||||
| const { | const { | ||||
| } = useNodesSyncDraft() | } = useNodesSyncDraft() | ||||
| const { workflowReadOnly } = useWorkflowReadOnly() | const { workflowReadOnly } = useWorkflowReadOnly() | ||||
| const { nodesReadOnly } = useNodesReadOnly() | const { nodesReadOnly } = useNodesReadOnly() | ||||
| const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) | |||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| eventEmitter?.useSubscription((v: any) => { | eventEmitter?.useSubscription((v: any) => { | ||||
| if (v.payload.viewport) | if (v.payload.viewport) | ||||
| reactflow.setViewport(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) | if (v.payload.hash) | ||||
| setSyncWorkflowDraftHash(v.payload.hash) | setSyncWorkflowDraftHash(v.payload.hash) | ||||
| onWorkflowDataUpdate?.(v.payload) | |||||
| setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) | setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) | ||||
| } | } | ||||
| if (v.type === DSL_EXPORT_CHECK) | |||||
| setSecretEnvList(v.payload.data as EnvironmentVariable[]) | |||||
| }) | }) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| }) | }) | ||||
| } | } | ||||
| }) | }) | ||||
| const { handleFetchAllTools } = useFetchToolsData() | |||||
| useEffect(() => { | |||||
| handleFetchAllTools('builtin') | |||||
| handleFetchAllTools('custom') | |||||
| handleFetchAllTools('workflow') | |||||
| }, [handleFetchAllTools]) | |||||
| const { | const { | ||||
| handleNodeDragStart, | handleNodeDragStart, | ||||
| } = useSelectionInteractions() | } = useSelectionInteractions() | ||||
| const { | const { | ||||
| handlePaneContextMenu, | handlePaneContextMenu, | ||||
| handlePaneContextmenuCancel, | |||||
| } = usePanelInteractions() | } = usePanelInteractions() | ||||
| const { | const { | ||||
| isValidConnection, | isValidConnection, | ||||
| } = useWorkflow() | } = useWorkflow() | ||||
| const { | |||||
| exportCheck, | |||||
| handleExportDSL, | |||||
| } = useDSL() | |||||
| useOnViewportChange({ | useOnViewportChange({ | ||||
| onEnd: () => { | onEnd: () => { | ||||
| > | > | ||||
| <SyncingDataModal /> | <SyncingDataModal /> | ||||
| <CandidateNode /> | <CandidateNode /> | ||||
| <Header /> | |||||
| <Panel /> | |||||
| <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> | <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> | ||||
| { | |||||
| showFeaturesPanel && <Features /> | |||||
| } | |||||
| <PanelContextmenu /> | <PanelContextmenu /> | ||||
| <NodeContextmenu /> | <NodeContextmenu /> | ||||
| <HelpLine /> | <HelpLine /> | ||||
| /> | /> | ||||
| ) | ) | ||||
| } | } | ||||
| { | |||||
| showImportDSLModal && ( | |||||
| <UpdateDSLModal | |||||
| onCancel={() => setShowImportDSLModal(false)} | |||||
| onBackup={exportCheck} | |||||
| onImport={handlePaneContextmenuCancel} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| secretEnvList.length > 0 && ( | |||||
| <DSLExportConfirmModal | |||||
| envList={secretEnvList} | |||||
| onConfirm={handleExportDSL} | |||||
| onClose={() => setSecretEnvList([])} | |||||
| /> | |||||
| ) | |||||
| } | |||||
| <LimitTips /> | <LimitTips /> | ||||
| <PluginDependency /> | |||||
| {children} | |||||
| <ReactFlow | <ReactFlow | ||||
| nodeTypes={nodeTypes} | nodeTypes={nodeTypes} | ||||
| edgeTypes={edgeTypes} | edgeTypes={edgeTypes} | ||||
| </div> | </div> | ||||
| ) | ) | ||||
| }) | }) | ||||
| Workflow.displayName = 'Workflow' | |||||
| 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 ( | |||||
| <div className='relative flex h-full w-full items-center justify-center'> | |||||
| <Loading /> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| type WorkflowWithInnerContextProps = WorkflowProps & { | |||||
| hooksStore?: Partial<HooksStoreShape> | |||||
| } | |||||
| export const WorkflowWithInnerContext = memo(({ | |||||
| hooksStore, | |||||
| ...restProps | |||||
| }: WorkflowWithInnerContextProps) => { | |||||
| return ( | |||||
| <HooksStoreContextProvider {...hooksStore}> | |||||
| <Workflow {...restProps} /> | |||||
| </HooksStoreContextProvider> | |||||
| ) | |||||
| }) | |||||
| 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 }, | |||||
| type WorkflowWithDefaultContextProps = | |||||
| Pick<WorkflowProps, 'edges' | 'nodes'> | |||||
| & { | |||||
| children: React.ReactNode | |||||
| } | } | ||||
| const WorkflowWithDefaultContext = ({ | |||||
| nodes, | |||||
| edges, | |||||
| children, | |||||
| }: WorkflowWithDefaultContextProps) => { | |||||
| return ( | return ( | ||||
| <ReactFlowProvider> | <ReactFlowProvider> | ||||
| <WorkflowHistoryProvider | <WorkflowHistoryProvider | ||||
| nodes={nodesData} | |||||
| edges={edgesData} > | |||||
| <FeaturesProvider features={initialFeatures}> | |||||
| <DatasetsDetailProvider nodes={nodesData}> | |||||
| <Workflow | |||||
| nodes={nodesData} | |||||
| edges={edgesData} | |||||
| viewport={data?.graph.viewport} | |||||
| /> | |||||
| </DatasetsDetailProvider> | |||||
| </FeaturesProvider> | |||||
| nodes={nodes} | |||||
| edges={edges} > | |||||
| <DatasetsDetailProvider nodes={nodes}> | |||||
| {children} | |||||
| </DatasetsDetailProvider> | |||||
| </WorkflowHistoryProvider> | </WorkflowHistoryProvider> | ||||
| </ReactFlowProvider> | </ReactFlowProvider> | ||||
| ) | ) | ||||
| }) | |||||
| WorkflowWrap.displayName = 'WorkflowWrap' | |||||
| const WorkflowContainer = () => { | |||||
| return ( | |||||
| <WorkflowContextProvider> | |||||
| <WorkflowWrap /> | |||||
| </WorkflowContextProvider> | |||||
| ) | |||||
| } | } | ||||
| export default memo(WorkflowContainer) | |||||
| export default memo(WorkflowWithDefaultContext) |
| import type { FC } from 'react' | import type { FC } from 'react' | ||||
| import { memo } from 'react' | import { memo } from 'react' | ||||
| import { useNodes } from 'reactflow' | import { useNodes } from 'reactflow' | ||||
| import { useShallow } from 'zustand/react/shallow' | |||||
| import type { CommonNodeType } from '../types' | import type { CommonNodeType } from '../types' | ||||
| import { Panel as NodePanel } from '../nodes' | import { Panel as NodePanel } from '../nodes' | ||||
| import { useStore } from '../store' | import { useStore } from '../store' | ||||
| import { | |||||
| useIsChatMode, | |||||
| } from '../hooks' | |||||
| import DebugAndPreview from './debug-and-preview' | |||||
| import Record from './record' | |||||
| import WorkflowPreview from './workflow-preview' | |||||
| import ChatRecord from './chat-record' | |||||
| import ChatVariablePanel from './chat-variable-panel' | |||||
| import EnvPanel from './env-panel' | import EnvPanel from './env-panel' | ||||
| import GlobalVariablePanel from './global-variable-panel' | |||||
| import VersionHistoryPanel from './version-history-panel' | |||||
| import cn from '@/utils/classnames' | import cn from '@/utils/classnames' | ||||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||||
| import MessageLogModal from '@/app/components/base/message-log-modal' | |||||
| const Panel: FC = () => { | |||||
| export type PanelProps = { | |||||
| components?: { | |||||
| left?: React.ReactNode | |||||
| right?: React.ReactNode | |||||
| } | |||||
| } | |||||
| const Panel: FC<PanelProps> = ({ | |||||
| components, | |||||
| }) => { | |||||
| const nodes = useNodes<CommonNodeType>() | const nodes = useNodes<CommonNodeType>() | ||||
| const isChatMode = useIsChatMode() | |||||
| const selectedNode = nodes.find(node => node.data.selected) | const selectedNode = nodes.find(node => node.data.selected) | ||||
| const historyWorkflowData = useStore(s => s.historyWorkflowData) | |||||
| const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel) | |||||
| const showEnvPanel = useStore(s => s.showEnvPanel) | const showEnvPanel = useStore(s => s.showEnvPanel) | ||||
| const showChatVariablePanel = useStore(s => s.showChatVariablePanel) | |||||
| const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel) | |||||
| const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel) | |||||
| const isRestoring = useStore(s => s.isRestoring) | const isRestoring = useStore(s => s.isRestoring) | ||||
| const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({ | |||||
| currentLogItem: state.currentLogItem, | |||||
| setCurrentLogItem: state.setCurrentLogItem, | |||||
| showMessageLogModal: state.showMessageLogModal, | |||||
| setShowMessageLogModal: state.setShowMessageLogModal, | |||||
| currentLogModalActiveTab: state.currentLogModalActiveTab, | |||||
| }))) | |||||
| return ( | return ( | ||||
| <div | <div | ||||
| key={`${isRestoring}`} | key={`${isRestoring}`} | ||||
| > | > | ||||
| { | { | ||||
| showMessageLogModal && ( | |||||
| <MessageLogModal | |||||
| fixedWidth | |||||
| width={400} | |||||
| currentLogItem={currentLogItem} | |||||
| onCancel={() => { | |||||
| setCurrentLogItem() | |||||
| setShowMessageLogModal(false) | |||||
| }} | |||||
| defaultTab={currentLogModalActiveTab} | |||||
| /> | |||||
| ) | |||||
| components?.left | |||||
| } | } | ||||
| { | { | ||||
| !!selectedNode && ( | !!selectedNode && ( | ||||
| ) | ) | ||||
| } | } | ||||
| { | { | ||||
| historyWorkflowData && !isChatMode && ( | |||||
| <Record /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| historyWorkflowData && isChatMode && ( | |||||
| <ChatRecord /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showDebugAndPreviewPanel && isChatMode && ( | |||||
| <DebugAndPreview /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showDebugAndPreviewPanel && !isChatMode && ( | |||||
| <WorkflowPreview /> | |||||
| ) | |||||
| components?.right | |||||
| } | } | ||||
| { | { | ||||
| showEnvPanel && ( | showEnvPanel && ( | ||||
| <EnvPanel /> | <EnvPanel /> | ||||
| ) | ) | ||||
| } | } | ||||
| { | |||||
| showChatVariablePanel && ( | |||||
| <ChatVariablePanel /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showGlobalVariablePanel && ( | |||||
| <GlobalVariablePanel /> | |||||
| ) | |||||
| } | |||||
| { | |||||
| showWorkflowVersionHistoryPanel && ( | |||||
| <VersionHistoryPanel/> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| import { useContext } from 'react' | import { useContext } from 'react' | ||||
| import type { | |||||
| StateCreator, | |||||
| } from 'zustand' | |||||
| import { | import { | ||||
| useStore as useZustandStore, | useStore as useZustandStore, | ||||
| } from 'zustand' | } from 'zustand' | ||||
| import type { WorkflowSliceShape } from './workflow-slice' | import type { WorkflowSliceShape } from './workflow-slice' | ||||
| import { createWorkflowSlice } from './workflow-slice' | import { createWorkflowSlice } from './workflow-slice' | ||||
| import { WorkflowContext } from '@/app/components/workflow/context' | import { WorkflowContext } from '@/app/components/workflow/context' | ||||
| import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice' | |||||
| export type Shape = | export type Shape = | ||||
| ChatVariableSliceShape & | ChatVariableSliceShape & | ||||
| ToolSliceShape & | ToolSliceShape & | ||||
| VersionSliceShape & | VersionSliceShape & | ||||
| WorkflowDraftSliceShape & | WorkflowDraftSliceShape & | ||||
| WorkflowSliceShape | |||||
| WorkflowSliceShape & | |||||
| WorkflowAppSliceShape | |||||
| type CreateWorkflowStoreParams = { | |||||
| injectWorkflowStoreSliceFn?: StateCreator<WorkflowAppSliceShape> | |||||
| } | |||||
| export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { | |||||
| const { injectWorkflowStoreSliceFn } = params || {} | |||||
| export const createWorkflowStore = () => { | |||||
| return createStore<Shape>((...args) => ({ | return createStore<Shape>((...args) => ({ | ||||
| ...createChatVariableSlice(...args), | ...createChatVariableSlice(...args), | ||||
| ...createEnvVariableSlice(...args), | ...createEnvVariableSlice(...args), | ||||
| ...createVersionSlice(...args), | ...createVersionSlice(...args), | ||||
| ...createWorkflowDraftSlice(...args), | ...createWorkflowDraftSlice(...args), | ||||
| ...createWorkflowSlice(...args), | ...createWorkflowSlice(...args), | ||||
| ...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape), | |||||
| })) | })) | ||||
| } | } | ||||
| export type NodeSliceShape = { | export type NodeSliceShape = { | ||||
| showSingleRunPanel: boolean | showSingleRunPanel: boolean | ||||
| setShowSingleRunPanel: (showSingleRunPanel: boolean) => void | setShowSingleRunPanel: (showSingleRunPanel: boolean) => void | ||||
| nodesDefaultConfigs: Record<string, any> | |||||
| setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void | |||||
| nodeAnimation: boolean | nodeAnimation: boolean | ||||
| setNodeAnimation: (nodeAnimation: boolean) => void | setNodeAnimation: (nodeAnimation: boolean) => void | ||||
| candidateNode?: Node | candidateNode?: Node | ||||
| export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({ | export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({ | ||||
| showSingleRunPanel: false, | showSingleRunPanel: false, | ||||
| setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), | setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), | ||||
| nodesDefaultConfigs: {}, | |||||
| setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), | |||||
| nodeAnimation: false, | nodeAnimation: false, | ||||
| setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })), | setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })), | ||||
| candidateNode: undefined, | candidateNode: undefined, |
| } | } | ||||
| export type WorkflowSliceShape = { | export type WorkflowSliceShape = { | ||||
| appId: string | |||||
| workflowRunningData?: PreviewRunningData | workflowRunningData?: PreviewRunningData | ||||
| setWorkflowRunningData: (workflowData: PreviewRunningData) => void | setWorkflowRunningData: (workflowData: PreviewRunningData) => void | ||||
| notInitialWorkflow: boolean | |||||
| setNotInitialWorkflow: (notInitialWorkflow: boolean) => void | |||||
| clipboardElements: Node[] | clipboardElements: Node[] | ||||
| setClipboardElements: (clipboardElements: Node[]) => void | setClipboardElements: (clipboardElements: Node[]) => void | ||||
| selection: null | { x1: number; y1: number; x2: number; y2: number } | selection: null | { x1: number; y1: number; x2: number; y2: number } | ||||
| setShowImportDSLModal: (showImportDSLModal: boolean) => void | setShowImportDSLModal: (showImportDSLModal: boolean) => void | ||||
| showTips: string | showTips: string | ||||
| setShowTips: (showTips: string) => void | setShowTips: (showTips: string) => void | ||||
| workflowConfig?: Record<string, any> | |||||
| setWorkflowConfig: (workflowConfig: Record<string, any>) => void | |||||
| } | } | ||||
| export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({ | export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({ | ||||
| appId: '', | |||||
| workflowRunningData: undefined, | workflowRunningData: undefined, | ||||
| setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), | setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), | ||||
| notInitialWorkflow: false, | |||||
| setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), | |||||
| clipboardElements: [], | clipboardElements: [], | ||||
| setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), | setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), | ||||
| selection: null, | selection: null, | ||||
| setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), | setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), | ||||
| showTips: '', | showTips: '', | ||||
| setShowTips: showTips => set(() => ({ showTips })), | setShowTips: showTips => set(() => ({ showTips })), | ||||
| workflowConfig: undefined, | |||||
| setWorkflowConfig: workflowConfig => set(() => ({ workflowConfig })), | |||||
| }) | }) |
| }) | }) | ||||
| } | } | ||||
| export const useWorkflowConfig = (appId: string) => { | |||||
| export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => { | |||||
| return useQuery({ | return useQuery({ | ||||
| queryKey: [NAME_SPACE, 'config', appId], | queryKey: [NAME_SPACE, 'config', appId], | ||||
| queryFn: () => get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`), | |||||
| queryFn: async () => { | |||||
| const data = await get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`) | |||||
| onSuccess(data) | |||||
| return data | |||||
| }, | |||||
| }) | }) | ||||
| } | } | ||||