| @@ -1,11 +1,11 @@ | |||
| 'use client' | |||
| import Workflow from '@/app/components/workflow' | |||
| import WorkflowApp from '@/app/components/workflow-app' | |||
| const Page = () => { | |||
| return ( | |||
| <div className='h-full w-full overflow-x-auto'> | |||
| <Workflow /> | |||
| <WorkflowApp /> | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,69 @@ | |||
| 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) | |||
| @@ -0,0 +1,11 @@ | |||
| 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) | |||
| @@ -0,0 +1,152 @@ | |||
| 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) | |||
| @@ -0,0 +1,31 @@ | |||
| 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 | |||
| @@ -0,0 +1,87 @@ | |||
| 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 | |||
| @@ -0,0 +1,109 @@ | |||
| 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 | |||
| @@ -0,0 +1,6 @@ | |||
| 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' | |||
| @@ -0,0 +1,7 @@ | |||
| import { useStore as useAppStore } from '@/app/components/app/store' | |||
| export const useIsChatMode = () => { | |||
| const appDetail = useAppStore(s => s.appDetail) | |||
| return appDetail?.mode === 'advanced-chat' | |||
| } | |||
| @@ -0,0 +1,148 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -0,0 +1,123 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -0,0 +1,357 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -0,0 +1,96 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -1,10 +1,10 @@ | |||
| import { generateNewNode } from '../utils' | |||
| import { generateNewNode } from '@/app/components/workflow/utils' | |||
| import { | |||
| NODE_WIDTH_X_OFFSET, | |||
| 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 = () => { | |||
| const isChatMode = useIsChatMode() | |||
| @@ -0,0 +1,108 @@ | |||
| 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 | |||
| @@ -0,0 +1,18 @@ | |||
| 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 })), | |||
| }) | |||
| @@ -2,19 +2,24 @@ import { | |||
| createContext, | |||
| useRef, | |||
| } 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> | |||
| export const WorkflowContext = createContext<WorkflowStore | null>(null) | |||
| type WorkflowProviderProps = { | |||
| export type WorkflowProviderProps = { | |||
| children: React.ReactNode | |||
| injectWorkflowStoreSliceFn?: StateCreator<WorkflowSliceShape> | |||
| } | |||
| export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => { | |||
| export const WorkflowContextProvider = ({ children, injectWorkflowStoreSliceFn }: WorkflowProviderProps) => { | |||
| const storeRef = useRef<WorkflowStore | undefined>(undefined) | |||
| if (!storeRef.current) | |||
| storeRef.current = createWorkflowStore() | |||
| storeRef.current = createWorkflowStore({ injectWorkflowStoreSliceFn }) | |||
| return ( | |||
| <WorkflowContext.Provider value={storeRef.current}> | |||
| @@ -1,13 +1,13 @@ | |||
| import { memo } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useWorkflow } from '../hooks' | |||
| import { useFormatTimeFromNow } from '../hooks' | |||
| import { useStore } from '@/app/components/workflow/store' | |||
| import useTimestamp from '@/hooks/use-timestamp' | |||
| const EditingTitle = () => { | |||
| const { t } = useTranslation() | |||
| const { formatTime } = useTimestamp() | |||
| const { formatTimeFromNow } = useWorkflow() | |||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||
| const draftUpdatedAt = useStore(state => state.draftUpdatedAt) | |||
| const publishedAt = useStore(state => state.publishedAt) | |||
| const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft) | |||
| @@ -0,0 +1,69 @@ | |||
| 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 | |||
| @@ -0,0 +1,93 @@ | |||
| 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 | |||
| @@ -0,0 +1,50 @@ | |||
| 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 | |||
| @@ -1,292 +1,51 @@ | |||
| import type { FC } from 'react' | |||
| 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, | |||
| useWorkflowRun, | |||
| } 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 { | |||
| normal, | |||
| restoring, | |||
| viewHistory, | |||
| } = 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 ( | |||
| <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' | |||
| > | |||
| <div> | |||
| { | |||
| normal && <EditingTitle /> | |||
| } | |||
| { | |||
| viewHistory && <RunningTitle /> | |||
| } | |||
| { | |||
| restoring && <RestoringTitle /> | |||
| } | |||
| </div> | |||
| { | |||
| 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 && ( | |||
| <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 && ( | |||
| <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> | |||
| ) | |||
| } | |||
| export default memo(Header) | |||
| export default Header | |||
| @@ -1,13 +1,13 @@ | |||
| import { memo, useMemo } from 'react' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useWorkflow } from '../hooks' | |||
| import { useFormatTimeFromNow } from '../hooks' | |||
| import { useStore } from '../store' | |||
| import { WorkflowVersion } from '../types' | |||
| import useTimestamp from '@/hooks/use-timestamp' | |||
| const RestoringTitle = () => { | |||
| const { t } = useTranslation() | |||
| const { formatTimeFromNow } = useWorkflow() | |||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||
| const { formatTime } = useTimestamp() | |||
| const currentVersion = useStore(state => state.currentVersion) | |||
| const isDraft = currentVersion?.version === WorkflowVersion.Draft | |||
| @@ -11,9 +11,9 @@ import { | |||
| RiErrorWarningLine, | |||
| } from '@remixicon/react' | |||
| import { | |||
| useFormatTimeFromNow, | |||
| useIsChatMode, | |||
| useNodesInteractions, | |||
| useWorkflow, | |||
| useWorkflowInteractions, | |||
| useWorkflowRun, | |||
| } from '../hooks' | |||
| @@ -50,7 +50,7 @@ const ViewHistory = ({ | |||
| const { t } = useTranslation() | |||
| const isChatMode = useIsChatMode() | |||
| const [open, setOpen] = useState(false) | |||
| const { formatTimeFromNow } = useWorkflow() | |||
| const { formatTimeFromNow } = useFormatTimeFromNow() | |||
| const { | |||
| handleNodesCancelSelected, | |||
| } = useNodesInteractions() | |||
| @@ -0,0 +1,2 @@ | |||
| export * from './provider' | |||
| export * from './store' | |||
| @@ -0,0 +1,36 @@ | |||
| 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> | |||
| ) | |||
| } | |||
| @@ -0,0 +1,72 @@ | |||
| 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)! | |||
| } | |||
| @@ -5,7 +5,6 @@ export * from './use-nodes-data' | |||
| export * from './use-nodes-sync-draft' | |||
| export * from './use-workflow' | |||
| export * from './use-workflow-run' | |||
| export * from './use-workflow-template' | |||
| export * from './use-checklist' | |||
| export * from './use-selection-interactions' | |||
| export * from './use-panel-interactions' | |||
| @@ -16,3 +15,4 @@ export * from './use-workflow-variables' | |||
| export * from './use-shortcuts' | |||
| export * from './use-workflow-interactions' | |||
| export * from './use-workflow-mode' | |||
| export * from './use-format-time-from-now' | |||
| @@ -0,0 +1,27 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -151,28 +151,11 @@ export const useEdgesInteractions = () => { | |||
| setEdges(newEdges) | |||
| }, [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 { | |||
| handleEdgeEnter, | |||
| handleEdgeLeave, | |||
| handleEdgeDeleteByDeleteBranch, | |||
| handleEdgeDelete, | |||
| handleEdgesChange, | |||
| handleEdgeCancelRunningStatus, | |||
| } | |||
| } | |||
| @@ -0,0 +1,12 @@ | |||
| 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 } | |||
| } | |||
| @@ -0,0 +1,27 @@ | |||
| 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, | |||
| } | |||
| } | |||
| @@ -1177,22 +1177,6 @@ export const useNodesInteractions = () => { | |||
| saveStateToHistory(WorkflowHistoryEvent.NodeChange) | |||
| }, [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 { | |||
| getNodes, | |||
| @@ -1554,7 +1538,6 @@ export const useNodesInteractions = () => { | |||
| handleNodeDelete, | |||
| handleNodeChange, | |||
| handleNodeAdd, | |||
| handleNodeCancelRunningStatus, | |||
| handleNodesCancelSelected, | |||
| handleNodeContextMenu, | |||
| handleNodesCopy, | |||
| @@ -1,147 +1,17 @@ | |||
| import { useCallback } from 'react' | |||
| import produce from 'immer' | |||
| import { useStoreApi } from 'reactflow' | |||
| import { useParams } from 'next/navigation' | |||
| import { | |||
| useStore, | |||
| useWorkflowStore, | |||
| } from '../store' | |||
| import { BlockEnum } from '../types' | |||
| import { useWorkflowUpdate } from '../hooks' | |||
| import { | |||
| useNodesReadOnly, | |||
| } 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 = () => { | |||
| const store = useStoreApi() | |||
| const workflowStore = useWorkflowStore() | |||
| const featuresStore = useFeaturesStore() | |||
| const { getNodesReadOnly } = useNodesReadOnly() | |||
| const { handleRefreshWorkflowDraft } = useWorkflowUpdate() | |||
| 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(( | |||
| sync?: boolean, | |||
| @@ -25,8 +25,8 @@ import { | |||
| useSelectionInteractions, | |||
| useWorkflowReadOnly, | |||
| } 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 { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| @@ -37,8 +37,8 @@ import { useStore as useAppStore } from '@/app/components/app/store' | |||
| export const useWorkflowInteractions = () => { | |||
| const workflowStore = useWorkflowStore() | |||
| const { handleNodeCancelRunningStatus } = useNodesInteractions() | |||
| const { handleEdgeCancelRunningStatus } = useEdgesInteractions() | |||
| const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync() | |||
| const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync() | |||
| const handleCancelDebugAndPreviewPanel = useCallback(() => { | |||
| workflowStore.setState({ | |||
| @@ -1,350 +1,11 @@ | |||
| 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 = () => { | |||
| 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 { | |||
| handleBackupDraft, | |||
| @@ -1,92 +1,9 @@ | |||
| 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 = () => { | |||
| 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 { | |||
| handleStartWorkflowRun, | |||
| @@ -1,13 +1,9 @@ | |||
| import { | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useState, | |||
| } from 'react' | |||
| import dayjs from 'dayjs' | |||
| import { uniqBy } from 'lodash-es' | |||
| import { useTranslation } from 'react-i18next' | |||
| import { useContext } from 'use-context-selector' | |||
| import { | |||
| getIncomers, | |||
| getOutgoers, | |||
| @@ -40,25 +36,15 @@ import { | |||
| import { CUSTOM_NOTE_NODE } from '../note-node/constants' | |||
| import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils' | |||
| import { useNodesExtraData } from './use-nodes-data' | |||
| 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 { | |||
| fetchAllBuiltInTools, | |||
| fetchAllCustomTools, | |||
| fetchAllWorkflowTools, | |||
| } from '@/service/tools' | |||
| import I18n from '@/context/i18n' | |||
| import { CollectionType } from '@/app/components/tools/types' | |||
| 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 { useWorkflowConfig } from '@/service/use-workflow' | |||
| import { basePath } from '@/utils/var' | |||
| import { canFindTool } from '@/utils' | |||
| @@ -70,12 +56,9 @@ export const useIsChatMode = () => { | |||
| export const useWorkflow = () => { | |||
| const { t } = useTranslation() | |||
| const { locale } = useContext(I18n) | |||
| const store = useStoreApi() | |||
| const workflowStore = useWorkflowStore() | |||
| const appId = useStore(s => s.appId) | |||
| const nodesExtraData = useNodesExtraData() | |||
| const { data: workflowConfig } = useWorkflowConfig(appId) | |||
| const setPanelWidth = useCallback((width: number) => { | |||
| localStorage.setItem('workflow-node-panel-width', `${width}`) | |||
| workflowStore.setState({ panelWidth: width }) | |||
| @@ -120,7 +103,7 @@ export const useWorkflow = () => { | |||
| 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) | |||
| }) | |||
| }, [store]) | |||
| @@ -167,7 +150,7 @@ export const useWorkflow = () => { | |||
| const length = list.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) | |||
| }) | |||
| } | |||
| @@ -344,6 +327,7 @@ export const useWorkflow = () => { | |||
| parallelList, | |||
| hasAbnormalEdges, | |||
| } = getParallelInfo(nodes, edges, parentNodeId) | |||
| const { workflowConfig } = workflowStore.getState() | |||
| if (hasAbnormalEdges) | |||
| return false | |||
| @@ -359,7 +343,7 @@ export const useWorkflow = () => { | |||
| } | |||
| return true | |||
| }, [t, workflowStore, workflowConfig?.parallel_depth_limit]) | |||
| }, [t, workflowStore]) | |||
| const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => { | |||
| const { | |||
| @@ -407,10 +391,6 @@ export const useWorkflow = () => { | |||
| return !hasCycle(targetNode) | |||
| }, [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 { getNodes } = store.getState() | |||
| const nodes = getNodes() | |||
| @@ -432,7 +412,6 @@ export const useWorkflow = () => { | |||
| checkNestedParallelLimit, | |||
| isValidConnection, | |||
| isFromStartNode, | |||
| formatTimeFromNow, | |||
| getNode, | |||
| getBeforeNodeById, | |||
| getIterationNodeChildren, | |||
| @@ -478,107 +457,6 @@ export const useFetchToolsData = () => { | |||
| } | |||
| } | |||
| 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 = () => { | |||
| const workflowStore = useWorkflowStore() | |||
| const workflowRunningData = useStore(s => s.workflowRunningData) | |||
| @@ -5,11 +5,8 @@ import { | |||
| memo, | |||
| useCallback, | |||
| useEffect, | |||
| useMemo, | |||
| useRef, | |||
| useState, | |||
| } from 'react' | |||
| import useSWR from 'swr' | |||
| import { setAutoFreeze } from 'immer' | |||
| import { | |||
| useEventListener, | |||
| @@ -31,17 +28,14 @@ import 'reactflow/dist/style.css' | |||
| import './style.css' | |||
| import type { | |||
| Edge, | |||
| EnvironmentVariable, | |||
| Node, | |||
| } from './types' | |||
| import { | |||
| ControlMode, | |||
| SupportUploadFileTypes, | |||
| } from './types' | |||
| import { WorkflowContextProvider } from './context' | |||
| import { | |||
| useDSL, | |||
| useEdgesInteractions, | |||
| useFetchToolsData, | |||
| useNodesInteractions, | |||
| useNodesReadOnly, | |||
| useNodesSyncDraft, | |||
| @@ -49,11 +43,9 @@ import { | |||
| useSelectionInteractions, | |||
| useShortcuts, | |||
| useWorkflow, | |||
| useWorkflowInit, | |||
| useWorkflowReadOnly, | |||
| useWorkflowUpdate, | |||
| } from './hooks' | |||
| import Header from './header' | |||
| import CustomNode from './nodes' | |||
| import CustomNoteNode from './note-node' | |||
| import { CUSTOM_NOTE_NODE } from './note-node/constants' | |||
| @@ -66,42 +58,28 @@ import { CUSTOM_SIMPLE_NODE } from './simple-node/constants' | |||
| import Operator from './operator' | |||
| import CustomEdge from './custom-edge' | |||
| import CustomConnectionLine from './custom-connection-line' | |||
| import Panel from './panel' | |||
| import Features from './features' | |||
| import HelpLine from './help-line' | |||
| import CandidateNode from './candidate-node' | |||
| import PanelContextmenu from './panel-contextmenu' | |||
| import NodeContextmenu from './node-contextmenu' | |||
| import SyncingDataModal from './syncing-data-modal' | |||
| import UpdateDSLModal from './update-dsl-modal' | |||
| import DSLExportConfirmModal from './dsl-export-confirm-modal' | |||
| import LimitTips from './limit-tips' | |||
| import PluginDependency from './plugin-dependency' | |||
| import { | |||
| useStore, | |||
| useWorkflowStore, | |||
| } from './store' | |||
| import { | |||
| initialEdges, | |||
| initialNodes, | |||
| } from './utils' | |||
| import { | |||
| CUSTOM_EDGE, | |||
| CUSTOM_NODE, | |||
| DSL_EXPORT_CHECK, | |||
| ITERATION_CHILDREN_Z_INDEX, | |||
| WORKFLOW_DATA_UPDATE, | |||
| } from './constants' | |||
| import { WorkflowHistoryProvider } from './workflow-history-store' | |||
| import Loading from '@/app/components/base/loading' | |||
| import { FeaturesProvider } from '@/app/components/base/features' | |||
| import type { Features as FeaturesData } from '@/app/components/base/features/types' | |||
| import { useFeaturesStore } from '@/app/components/base/features/hooks' | |||
| import { useEventEmitterContextContext } from '@/context/event-emitter' | |||
| import Confirm from '@/app/components/base/confirm' | |||
| import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants' | |||
| import { fetchFileUploadConfig } from '@/service/common' | |||
| import DatasetsDetailProvider from './datasets-detail-store/provider' | |||
| import { HooksStoreContextProvider } from './hooks-store' | |||
| import type { Shape as HooksStoreShape } from './hooks-store' | |||
| const nodeTypes = { | |||
| [CUSTOM_NODE]: CustomNode, | |||
| @@ -114,32 +92,32 @@ const edgeTypes = { | |||
| [CUSTOM_EDGE]: CustomEdge, | |||
| } | |||
| type WorkflowProps = { | |||
| export type WorkflowProps = { | |||
| nodes: Node[] | |||
| edges: Edge[] | |||
| viewport?: Viewport | |||
| children?: React.ReactNode | |||
| onWorkflowDataUpdate?: (v: any) => void | |||
| } | |||
| const Workflow: FC<WorkflowProps> = memo(({ | |||
| export const Workflow: FC<WorkflowProps> = memo(({ | |||
| nodes: originalNodes, | |||
| edges: originalEdges, | |||
| viewport, | |||
| children, | |||
| onWorkflowDataUpdate, | |||
| }) => { | |||
| const workflowContainerRef = useRef<HTMLDivElement>(null) | |||
| const workflowStore = useWorkflowStore() | |||
| const reactflow = useReactFlow() | |||
| const featuresStore = useFeaturesStore() | |||
| const [nodes, setNodes] = useNodesState(originalNodes) | |||
| const [edges, setEdges] = useEdgesState(originalEdges) | |||
| const showFeaturesPanel = useStore(state => state.showFeaturesPanel) | |||
| const controlMode = useStore(s => s.controlMode) | |||
| const nodeAnimation = useStore(s => s.nodeAnimation) | |||
| const showConfirm = useStore(s => s.showConfirm) | |||
| const showImportDSLModal = useStore(s => s.showImportDSLModal) | |||
| const { | |||
| setShowConfirm, | |||
| setControlPromptEditorRerenderKey, | |||
| setShowImportDSLModal, | |||
| setSyncWorkflowDraftHash, | |||
| } = workflowStore.getState() | |||
| const { | |||
| @@ -148,9 +126,6 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| } = useNodesSyncDraft() | |||
| const { workflowReadOnly } = useWorkflowReadOnly() | |||
| const { nodesReadOnly } = useNodesReadOnly() | |||
| const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([]) | |||
| const { eventEmitter } = useEventEmitterContextContext() | |||
| eventEmitter?.useSubscription((v: any) => { | |||
| @@ -161,19 +136,13 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| if (v.payload.viewport) | |||
| reactflow.setViewport(v.payload.viewport) | |||
| if (v.payload.features && featuresStore) { | |||
| const { setFeatures } = featuresStore.getState() | |||
| setFeatures(v.payload.features) | |||
| } | |||
| if (v.payload.hash) | |||
| setSyncWorkflowDraftHash(v.payload.hash) | |||
| onWorkflowDataUpdate?.(v.payload) | |||
| setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) | |||
| } | |||
| if (v.type === DSL_EXPORT_CHECK) | |||
| setSecretEnvList(v.payload.data as EnvironmentVariable[]) | |||
| }) | |||
| useEffect(() => { | |||
| @@ -231,6 +200,12 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| }) | |||
| } | |||
| }) | |||
| const { handleFetchAllTools } = useFetchToolsData() | |||
| useEffect(() => { | |||
| handleFetchAllTools('builtin') | |||
| handleFetchAllTools('custom') | |||
| handleFetchAllTools('workflow') | |||
| }, [handleFetchAllTools]) | |||
| const { | |||
| handleNodeDragStart, | |||
| @@ -258,15 +233,10 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| } = useSelectionInteractions() | |||
| const { | |||
| handlePaneContextMenu, | |||
| handlePaneContextmenuCancel, | |||
| } = usePanelInteractions() | |||
| const { | |||
| isValidConnection, | |||
| } = useWorkflow() | |||
| const { | |||
| exportCheck, | |||
| handleExportDSL, | |||
| } = useDSL() | |||
| useOnViewportChange({ | |||
| onEnd: () => { | |||
| @@ -297,12 +267,7 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| > | |||
| <SyncingDataModal /> | |||
| <CandidateNode /> | |||
| <Header /> | |||
| <Panel /> | |||
| <Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} /> | |||
| { | |||
| showFeaturesPanel && <Features /> | |||
| } | |||
| <PanelContextmenu /> | |||
| <NodeContextmenu /> | |||
| <HelpLine /> | |||
| @@ -317,26 +282,8 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| showImportDSLModal && ( | |||
| <UpdateDSLModal | |||
| onCancel={() => setShowImportDSLModal(false)} | |||
| onBackup={exportCheck} | |||
| onImport={handlePaneContextmenuCancel} | |||
| /> | |||
| ) | |||
| } | |||
| { | |||
| secretEnvList.length > 0 && ( | |||
| <DSLExportConfirmModal | |||
| envList={secretEnvList} | |||
| onConfirm={handleExportDSL} | |||
| onClose={() => setSecretEnvList([])} | |||
| /> | |||
| ) | |||
| } | |||
| <LimitTips /> | |||
| <PluginDependency /> | |||
| {children} | |||
| <ReactFlow | |||
| nodeTypes={nodeTypes} | |||
| edgeTypes={edgeTypes} | |||
| @@ -389,89 +336,43 @@ const Workflow: FC<WorkflowProps> = memo(({ | |||
| </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 ( | |||
| <ReactFlowProvider> | |||
| <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> | |||
| </ReactFlowProvider> | |||
| ) | |||
| }) | |||
| WorkflowWrap.displayName = 'WorkflowWrap' | |||
| const WorkflowContainer = () => { | |||
| return ( | |||
| <WorkflowContextProvider> | |||
| <WorkflowWrap /> | |||
| </WorkflowContextProvider> | |||
| ) | |||
| } | |||
| export default memo(WorkflowContainer) | |||
| export default memo(WorkflowWithDefaultContext) | |||
| @@ -1,43 +1,25 @@ | |||
| import type { FC } from 'react' | |||
| import { memo } from 'react' | |||
| import { useNodes } from 'reactflow' | |||
| import { useShallow } from 'zustand/react/shallow' | |||
| import type { CommonNodeType } from '../types' | |||
| import { Panel as NodePanel } from '../nodes' | |||
| 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 GlobalVariablePanel from './global-variable-panel' | |||
| import VersionHistoryPanel from './version-history-panel' | |||
| 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 isChatMode = useIsChatMode() | |||
| 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 showChatVariablePanel = useStore(s => s.showChatVariablePanel) | |||
| const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel) | |||
| const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel) | |||
| 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 ( | |||
| <div | |||
| @@ -46,18 +28,7 @@ const Panel: FC = () => { | |||
| key={`${isRestoring}`} | |||
| > | |||
| { | |||
| showMessageLogModal && ( | |||
| <MessageLogModal | |||
| fixedWidth | |||
| width={400} | |||
| currentLogItem={currentLogItem} | |||
| onCancel={() => { | |||
| setCurrentLogItem() | |||
| setShowMessageLogModal(false) | |||
| }} | |||
| defaultTab={currentLogModalActiveTab} | |||
| /> | |||
| ) | |||
| components?.left | |||
| } | |||
| { | |||
| !!selectedNode && ( | |||
| @@ -65,45 +36,13 @@ const Panel: FC = () => { | |||
| ) | |||
| } | |||
| { | |||
| historyWorkflowData && !isChatMode && ( | |||
| <Record /> | |||
| ) | |||
| } | |||
| { | |||
| historyWorkflowData && isChatMode && ( | |||
| <ChatRecord /> | |||
| ) | |||
| } | |||
| { | |||
| showDebugAndPreviewPanel && isChatMode && ( | |||
| <DebugAndPreview /> | |||
| ) | |||
| } | |||
| { | |||
| showDebugAndPreviewPanel && !isChatMode && ( | |||
| <WorkflowPreview /> | |||
| ) | |||
| components?.right | |||
| } | |||
| { | |||
| showEnvPanel && ( | |||
| <EnvPanel /> | |||
| ) | |||
| } | |||
| { | |||
| showChatVariablePanel && ( | |||
| <ChatVariablePanel /> | |||
| ) | |||
| } | |||
| { | |||
| showGlobalVariablePanel && ( | |||
| <GlobalVariablePanel /> | |||
| ) | |||
| } | |||
| { | |||
| showWorkflowVersionHistoryPanel && ( | |||
| <VersionHistoryPanel/> | |||
| ) | |||
| } | |||
| </div> | |||
| ) | |||
| } | |||
| @@ -1,4 +1,7 @@ | |||
| import { useContext } from 'react' | |||
| import type { | |||
| StateCreator, | |||
| } from 'zustand' | |||
| import { | |||
| useStore as useZustandStore, | |||
| } from 'zustand' | |||
| @@ -26,6 +29,7 @@ import { createWorkflowDraftSlice } from './workflow-draft-slice' | |||
| import type { WorkflowSliceShape } from './workflow-slice' | |||
| import { createWorkflowSlice } from './workflow-slice' | |||
| import { WorkflowContext } from '@/app/components/workflow/context' | |||
| import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice' | |||
| export type Shape = | |||
| ChatVariableSliceShape & | |||
| @@ -38,9 +42,16 @@ export type Shape = | |||
| ToolSliceShape & | |||
| VersionSliceShape & | |||
| WorkflowDraftSliceShape & | |||
| WorkflowSliceShape | |||
| WorkflowSliceShape & | |||
| WorkflowAppSliceShape | |||
| type CreateWorkflowStoreParams = { | |||
| injectWorkflowStoreSliceFn?: StateCreator<WorkflowAppSliceShape> | |||
| } | |||
| export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { | |||
| const { injectWorkflowStoreSliceFn } = params || {} | |||
| export const createWorkflowStore = () => { | |||
| return createStore<Shape>((...args) => ({ | |||
| ...createChatVariableSlice(...args), | |||
| ...createEnvVariableSlice(...args), | |||
| @@ -53,6 +64,7 @@ export const createWorkflowStore = () => { | |||
| ...createVersionSlice(...args), | |||
| ...createWorkflowDraftSlice(...args), | |||
| ...createWorkflowSlice(...args), | |||
| ...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape), | |||
| })) | |||
| } | |||
| @@ -12,8 +12,6 @@ import type { | |||
| export type NodeSliceShape = { | |||
| showSingleRunPanel: boolean | |||
| setShowSingleRunPanel: (showSingleRunPanel: boolean) => void | |||
| nodesDefaultConfigs: Record<string, any> | |||
| setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void | |||
| nodeAnimation: boolean | |||
| setNodeAnimation: (nodeAnimation: boolean) => void | |||
| candidateNode?: Node | |||
| @@ -55,8 +53,6 @@ export type NodeSliceShape = { | |||
| export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({ | |||
| showSingleRunPanel: false, | |||
| setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })), | |||
| nodesDefaultConfigs: {}, | |||
| setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })), | |||
| nodeAnimation: false, | |||
| setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })), | |||
| candidateNode: undefined, | |||
| @@ -10,11 +10,8 @@ type PreviewRunningData = WorkflowRunningData & { | |||
| } | |||
| export type WorkflowSliceShape = { | |||
| appId: string | |||
| workflowRunningData?: PreviewRunningData | |||
| setWorkflowRunningData: (workflowData: PreviewRunningData) => void | |||
| notInitialWorkflow: boolean | |||
| setNotInitialWorkflow: (notInitialWorkflow: boolean) => void | |||
| clipboardElements: Node[] | |||
| setClipboardElements: (clipboardElements: Node[]) => void | |||
| selection: null | { x1: number; y1: number; x2: number; y2: number } | |||
| @@ -33,14 +30,13 @@ export type WorkflowSliceShape = { | |||
| setShowImportDSLModal: (showImportDSLModal: boolean) => void | |||
| showTips: string | |||
| setShowTips: (showTips: string) => void | |||
| workflowConfig?: Record<string, any> | |||
| setWorkflowConfig: (workflowConfig: Record<string, any>) => void | |||
| } | |||
| export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({ | |||
| appId: '', | |||
| workflowRunningData: undefined, | |||
| setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })), | |||
| notInitialWorkflow: false, | |||
| setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })), | |||
| clipboardElements: [], | |||
| setClipboardElements: clipboardElements => set(() => ({ clipboardElements })), | |||
| selection: null, | |||
| @@ -62,4 +58,6 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({ | |||
| setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })), | |||
| showTips: '', | |||
| setShowTips: showTips => set(() => ({ showTips })), | |||
| workflowConfig: undefined, | |||
| setWorkflowConfig: workflowConfig => set(() => ({ workflowConfig })), | |||
| }) | |||
| @@ -21,10 +21,14 @@ export const useAppWorkflow = (appID: string) => { | |||
| }) | |||
| } | |||
| export const useWorkflowConfig = (appId: string) => { | |||
| export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => { | |||
| return useQuery({ | |||
| 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 | |||
| }, | |||
| }) | |||
| } | |||