Merge branch 'langgenius:main' into fix-doc-TOC-style
commit
e9cb9e8695
@ -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 {
|
import {
|
||||||
NODE_WIDTH_X_OFFSET,
|
NODE_WIDTH_X_OFFSET,
|
||||||
START_INITIAL_POSITION,
|
START_INITIAL_POSITION,
|
||||||
} from '../constants'
|
} from '@/app/components/workflow/constants'
|
||||||
import { useIsChatMode } from './use-workflow'
|
import { useNodesInitialData } from '@/app/components/workflow/hooks'
|
||||||
import { useNodesInitialData } from './use-nodes-data'
|
import { useIsChatMode } from './use-is-chat-mode'
|
||||||
|
|
||||||
export const useWorkflowTemplate = () => {
|
export const useWorkflowTemplate = () => {
|
||||||
const isChatMode = useIsChatMode()
|
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 })),
|
||||||
|
})
|
||||||
@ -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 {
|
import {
|
||||||
memo,
|
|
||||||
useCallback,
|
|
||||||
useMemo,
|
|
||||||
} from 'react'
|
|
||||||
import { RiApps2AddLine, RiHistoryLine } from '@remixicon/react'
|
|
||||||
import { useNodes } from 'reactflow'
|
|
||||||
import { useTranslation } from 'react-i18next'
|
|
||||||
import { useContext, useContextSelector } from 'use-context-selector'
|
|
||||||
import {
|
|
||||||
useStore,
|
|
||||||
useWorkflowStore,
|
|
||||||
} from '../store'
|
|
||||||
import {
|
|
||||||
BlockEnum,
|
|
||||||
InputVarType,
|
|
||||||
WorkflowVersion,
|
|
||||||
} from '../types'
|
|
||||||
import type { StartNodeType } from '../nodes/start/types'
|
|
||||||
import {
|
|
||||||
useChecklistBeforePublish,
|
|
||||||
useIsChatMode,
|
|
||||||
useNodesInteractions,
|
|
||||||
useNodesReadOnly,
|
|
||||||
useNodesSyncDraft,
|
|
||||||
useWorkflowMode,
|
useWorkflowMode,
|
||||||
useWorkflowRun,
|
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import AppPublisher from '../../app/app-publisher'
|
import type { HeaderInNormalProps } from './header-in-normal'
|
||||||
import Toast, { ToastContext } from '../../base/toast'
|
import HeaderInNormal from './header-in-normal'
|
||||||
import Divider from '../../base/divider'
|
import HeaderInHistory from './header-in-view-history'
|
||||||
import RunAndHistory from './run-and-history'
|
import type { HeaderInRestoringProps } from './header-in-restoring'
|
||||||
import EditingTitle from './editing-title'
|
import HeaderInRestoring from './header-in-restoring'
|
||||||
import RunningTitle from './running-title'
|
|
||||||
import RestoringTitle from './restoring-title'
|
export type HeaderProps = {
|
||||||
import ViewHistory from './view-history'
|
normal?: HeaderInNormalProps
|
||||||
import ChatVariableButton from './chat-variable-button'
|
restoring?: HeaderInRestoringProps
|
||||||
import EnvButton from './env-button'
|
}
|
||||||
import VersionHistoryButton from './version-history-button'
|
const Header = ({
|
||||||
import Button from '@/app/components/base/button'
|
normal: normalProps,
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
restoring: restoringProps,
|
||||||
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
|
}: HeaderProps) => {
|
||||||
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)
|
|
||||||
const {
|
const {
|
||||||
normal,
|
normal,
|
||||||
restoring,
|
restoring,
|
||||||
viewHistory,
|
viewHistory,
|
||||||
} = useWorkflowMode()
|
} = useWorkflowMode()
|
||||||
|
|
||||||
const handleShowFeatures = useCallback(() => {
|
|
||||||
const {
|
|
||||||
showFeaturesPanel,
|
|
||||||
isRestoring,
|
|
||||||
setShowFeaturesPanel,
|
|
||||||
} = workflowStore.getState()
|
|
||||||
if (getNodesReadOnly() && !isRestoring)
|
|
||||||
return
|
|
||||||
setShowFeaturesPanel(!showFeaturesPanel)
|
|
||||||
}, [workflowStore, getNodesReadOnly])
|
|
||||||
|
|
||||||
const handleCancelRestore = useCallback(() => {
|
|
||||||
handleLoadBackupDraft()
|
|
||||||
workflowStore.setState({ isRestoring: false })
|
|
||||||
setShowWorkflowVersionHistoryPanel(false)
|
|
||||||
}, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel])
|
|
||||||
|
|
||||||
const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)
|
|
||||||
|
|
||||||
const handleRestore = useCallback(() => {
|
|
||||||
setShowWorkflowVersionHistoryPanel(false)
|
|
||||||
workflowStore.setState({ isRestoring: false })
|
|
||||||
workflowStore.setState({ backupDraft: undefined })
|
|
||||||
handleSyncWorkflowDraft(true, false, {
|
|
||||||
onSuccess: () => {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'success',
|
|
||||||
message: t('workflow.versionHistory.action.restoreSuccess'),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onError: () => {
|
|
||||||
Toast.notify({
|
|
||||||
type: 'error',
|
|
||||||
message: t('workflow.versionHistory.action.restoreFailure'),
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onSettled: () => {
|
|
||||||
resetWorkflowVersionHistory()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
}, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, resetWorkflowVersionHistory, t])
|
|
||||||
|
|
||||||
const updateAppDetail = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const res = await fetchAppDetail({ url: '/apps', id: appID! })
|
|
||||||
if (systemFeatures.enable_web_sso_switch_component) {
|
|
||||||
const ssoRes = await fetchAppSSO({ appId: appID! })
|
|
||||||
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
setAppDetail({ ...res })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.error(error)
|
|
||||||
}
|
|
||||||
}, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component])
|
|
||||||
|
|
||||||
const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)
|
|
||||||
|
|
||||||
const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
|
|
||||||
if (await handleCheckBeforePublish()) {
|
|
||||||
const res = await publishWorkflow({
|
|
||||||
title: params?.title || '',
|
|
||||||
releaseNotes: params?.releaseNotes || '',
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res) {
|
|
||||||
notify({ type: 'success', message: t('common.api.actionSuccess') })
|
|
||||||
updateAppDetail()
|
|
||||||
workflowStore.getState().setPublishedAt(res.created_at)
|
|
||||||
resetWorkflowVersionHistory()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
throw new Error('Checklist failed')
|
|
||||||
}
|
|
||||||
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])
|
|
||||||
|
|
||||||
const onStartRestoring = useCallback(() => {
|
|
||||||
workflowStore.setState({ isRestoring: true })
|
|
||||||
handleBackupDraft()
|
|
||||||
// clear right panel
|
|
||||||
if (selectedNode)
|
|
||||||
handleNodeSelect(selectedNode.id, true)
|
|
||||||
setShowWorkflowVersionHistoryPanel(true)
|
|
||||||
setShowEnvPanel(false)
|
|
||||||
setShowDebugAndPreviewPanel(false)
|
|
||||||
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode,
|
|
||||||
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel])
|
|
||||||
|
|
||||||
const onPublisherToggle = useCallback((state: boolean) => {
|
|
||||||
if (state)
|
|
||||||
handleSyncWorkflowDraft(true)
|
|
||||||
}, [handleSyncWorkflowDraft])
|
|
||||||
|
|
||||||
const handleGoBackToEdit = useCallback(() => {
|
|
||||||
handleLoadBackupDraft()
|
|
||||||
workflowStore.setState({ historyWorkflowData: undefined })
|
|
||||||
}, [workflowStore, handleLoadBackupDraft])
|
|
||||||
|
|
||||||
const handleToolConfigureUpdate = useCallback(() => {
|
|
||||||
workflowStore.setState({ toolPublished: true })
|
|
||||||
}, [workflowStore])
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
{
|
|
||||||
normal && <EditingTitle />
|
|
||||||
}
|
|
||||||
{
|
|
||||||
viewHistory && <RunningTitle />
|
|
||||||
}
|
|
||||||
{
|
|
||||||
restoring && <RestoringTitle />
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
{
|
{
|
||||||
normal && (
|
normal && (
|
||||||
<div className='flex items-center gap-2'>
|
<HeaderInNormal
|
||||||
{/* <GlobalVariableButton disabled={nodesReadOnly} /> */}
|
{...normalProps}
|
||||||
{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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
viewHistory && (
|
viewHistory && (
|
||||||
<div className='flex items-center space-x-2'>
|
<HeaderInHistory />
|
||||||
<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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
restoring && (
|
restoring && (
|
||||||
<div className='flex items-center justify-end gap-x-2'>
|
<HeaderInRestoring
|
||||||
<Button
|
{...restoringProps}
|
||||||
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>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
export default memo(Header)
|
export default Header
|
||||||
|
|||||||
@ -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)!
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue