mirror of https://github.com/langgenius/dify.git
merge chore/slice-workflow
This commit is contained in:
commit
f98c721cd5
|
@ -1,11 +1,11 @@
|
||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import Workflow from '@/app/components/workflow'
|
import WorkflowApp from '@/app/components/workflow-app'
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className='h-full w-full overflow-x-auto'>
|
<div className='h-full w-full overflow-x-auto'>
|
||||||
<Workflow />
|
<WorkflowApp />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 })),
|
||||||
|
})
|
|
@ -2,19 +2,24 @@ import {
|
||||||
createContext,
|
createContext,
|
||||||
useRef,
|
useRef,
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import { createWorkflowStore } from './store'
|
import {
|
||||||
|
createWorkflowStore,
|
||||||
|
} from './store'
|
||||||
|
import type { StateCreator } from 'zustand'
|
||||||
|
import type { WorkflowSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice'
|
||||||
|
|
||||||
type WorkflowStore = ReturnType<typeof createWorkflowStore>
|
type WorkflowStore = ReturnType<typeof createWorkflowStore>
|
||||||
export const WorkflowContext = createContext<WorkflowStore | null>(null)
|
export const WorkflowContext = createContext<WorkflowStore | null>(null)
|
||||||
|
|
||||||
type WorkflowProviderProps = {
|
export type WorkflowProviderProps = {
|
||||||
children: React.ReactNode
|
children: React.ReactNode
|
||||||
|
injectWorkflowStoreSliceFn?: StateCreator<WorkflowSliceShape>
|
||||||
}
|
}
|
||||||
export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => {
|
export const WorkflowContextProvider = ({ children, injectWorkflowStoreSliceFn }: WorkflowProviderProps) => {
|
||||||
const storeRef = useRef<WorkflowStore | undefined>(undefined)
|
const storeRef = useRef<WorkflowStore | undefined>(undefined)
|
||||||
|
|
||||||
if (!storeRef.current)
|
if (!storeRef.current)
|
||||||
storeRef.current = createWorkflowStore()
|
storeRef.current = createWorkflowStore({ injectWorkflowStoreSliceFn })
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<WorkflowContext.Provider value={storeRef.current}>
|
<WorkflowContext.Provider value={storeRef.current}>
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWorkflow } from '../hooks'
|
import { useFormatTimeFromNow } from '../hooks'
|
||||||
import { useStore } from '@/app/components/workflow/store'
|
import { useStore } from '@/app/components/workflow/store'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
|
|
||||||
const EditingTitle = () => {
|
const EditingTitle = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const { formatTimeFromNow } = useWorkflow()
|
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||||
const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
|
const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
|
||||||
const publishedAt = useStore(state => state.publishedAt)
|
const publishedAt = useStore(state => state.publishedAt)
|
||||||
const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft)
|
const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft)
|
||||||
|
|
|
@ -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'
|
|
||||||
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 = () => {
|
export type HeaderProps = {
|
||||||
const { t } = useTranslation()
|
normal?: HeaderInNormalProps
|
||||||
const workflowStore = useWorkflowStore()
|
restoring?: HeaderInRestoringProps
|
||||||
const appDetail = useAppStore(s => s.appDetail)
|
}
|
||||||
const setAppDetail = useAppStore(s => s.setAppDetail)
|
const Header = ({
|
||||||
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
|
normal: normalProps,
|
||||||
const appID = appDetail?.id
|
restoring: restoringProps,
|
||||||
const isChatMode = useIsChatMode()
|
}: HeaderProps) => {
|
||||||
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
|
||||||
|
|
|
@ -1,13 +1,13 @@
|
||||||
import { memo, useMemo } from 'react'
|
import { memo, useMemo } from 'react'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useWorkflow } from '../hooks'
|
import { useFormatTimeFromNow } from '../hooks'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import { WorkflowVersion } from '../types'
|
import { WorkflowVersion } from '../types'
|
||||||
import useTimestamp from '@/hooks/use-timestamp'
|
import useTimestamp from '@/hooks/use-timestamp'
|
||||||
|
|
||||||
const RestoringTitle = () => {
|
const RestoringTitle = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { formatTimeFromNow } = useWorkflow()
|
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||||
const { formatTime } = useTimestamp()
|
const { formatTime } = useTimestamp()
|
||||||
const currentVersion = useStore(state => state.currentVersion)
|
const currentVersion = useStore(state => state.currentVersion)
|
||||||
const isDraft = currentVersion?.version === WorkflowVersion.Draft
|
const isDraft = currentVersion?.version === WorkflowVersion.Draft
|
||||||
|
|
|
@ -11,9 +11,9 @@ import {
|
||||||
RiErrorWarningLine,
|
RiErrorWarningLine,
|
||||||
} from '@remixicon/react'
|
} from '@remixicon/react'
|
||||||
import {
|
import {
|
||||||
|
useFormatTimeFromNow,
|
||||||
useIsChatMode,
|
useIsChatMode,
|
||||||
useNodesInteractions,
|
useNodesInteractions,
|
||||||
useWorkflow,
|
|
||||||
useWorkflowInteractions,
|
useWorkflowInteractions,
|
||||||
useWorkflowRun,
|
useWorkflowRun,
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
|
@ -50,7 +50,7 @@ const ViewHistory = ({
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const isChatMode = useIsChatMode()
|
const isChatMode = useIsChatMode()
|
||||||
const [open, setOpen] = useState(false)
|
const [open, setOpen] = useState(false)
|
||||||
const { formatTimeFromNow } = useWorkflow()
|
const { formatTimeFromNow } = useFormatTimeFromNow()
|
||||||
const {
|
const {
|
||||||
handleNodesCancelSelected,
|
handleNodesCancelSelected,
|
||||||
} = useNodesInteractions()
|
} = useNodesInteractions()
|
||||||
|
|
|
@ -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-nodes-sync-draft'
|
||||||
export * from './use-workflow'
|
export * from './use-workflow'
|
||||||
export * from './use-workflow-run'
|
export * from './use-workflow-run'
|
||||||
export * from './use-workflow-template'
|
|
||||||
export * from './use-checklist'
|
export * from './use-checklist'
|
||||||
export * from './use-selection-interactions'
|
export * from './use-selection-interactions'
|
||||||
export * from './use-panel-interactions'
|
export * from './use-panel-interactions'
|
||||||
|
@ -16,3 +15,4 @@ export * from './use-workflow-variables'
|
||||||
export * from './use-shortcuts'
|
export * from './use-shortcuts'
|
||||||
export * from './use-workflow-interactions'
|
export * from './use-workflow-interactions'
|
||||||
export * from './use-workflow-mode'
|
export * from './use-workflow-mode'
|
||||||
|
export * from './use-format-time-from-now'
|
||||||
|
|
|
@ -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)
|
setEdges(newEdges)
|
||||||
}, [store, getNodesReadOnly])
|
}, [store, getNodesReadOnly])
|
||||||
|
|
||||||
const handleEdgeCancelRunningStatus = useCallback(() => {
|
|
||||||
const {
|
|
||||||
edges,
|
|
||||||
setEdges,
|
|
||||||
} = store.getState()
|
|
||||||
|
|
||||||
const newEdges = produce(edges, (draft) => {
|
|
||||||
draft.forEach((edge) => {
|
|
||||||
edge.data._sourceRunningStatus = undefined
|
|
||||||
edge.data._targetRunningStatus = undefined
|
|
||||||
edge.data._waitingRun = false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
setEdges(newEdges)
|
|
||||||
}, [store])
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
handleEdgeEnter,
|
handleEdgeEnter,
|
||||||
handleEdgeLeave,
|
handleEdgeLeave,
|
||||||
handleEdgeDeleteByDeleteBranch,
|
handleEdgeDeleteByDeleteBranch,
|
||||||
handleEdgeDelete,
|
handleEdgeDelete,
|
||||||
handleEdgesChange,
|
handleEdgesChange,
|
||||||
handleEdgeCancelRunningStatus,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
saveStateToHistory(WorkflowHistoryEvent.NodeChange)
|
||||||
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory])
|
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory])
|
||||||
|
|
||||||
const handleNodeCancelRunningStatus = useCallback(() => {
|
|
||||||
const {
|
|
||||||
getNodes,
|
|
||||||
setNodes,
|
|
||||||
} = store.getState()
|
|
||||||
|
|
||||||
const nodes = getNodes()
|
|
||||||
const newNodes = produce(nodes, (draft) => {
|
|
||||||
draft.forEach((node) => {
|
|
||||||
node.data._runningStatus = undefined
|
|
||||||
node.data._waitingRun = false
|
|
||||||
})
|
|
||||||
})
|
|
||||||
setNodes(newNodes)
|
|
||||||
}, [store])
|
|
||||||
|
|
||||||
const handleNodesCancelSelected = useCallback(() => {
|
const handleNodesCancelSelected = useCallback(() => {
|
||||||
const {
|
const {
|
||||||
getNodes,
|
getNodes,
|
||||||
|
@ -1554,7 +1538,6 @@ export const useNodesInteractions = () => {
|
||||||
handleNodeDelete,
|
handleNodeDelete,
|
||||||
handleNodeChange,
|
handleNodeChange,
|
||||||
handleNodeAdd,
|
handleNodeAdd,
|
||||||
handleNodeCancelRunningStatus,
|
|
||||||
handleNodesCancelSelected,
|
handleNodesCancelSelected,
|
||||||
handleNodeContextMenu,
|
handleNodeContextMenu,
|
||||||
handleNodesCopy,
|
handleNodesCopy,
|
||||||
|
|
|
@ -1,147 +1,17 @@
|
||||||
import { useCallback } from 'react'
|
import { useCallback } from 'react'
|
||||||
import produce from 'immer'
|
|
||||||
import { useStoreApi } from 'reactflow'
|
|
||||||
import { useParams } from 'next/navigation'
|
|
||||||
import {
|
import {
|
||||||
useStore,
|
useStore,
|
||||||
useWorkflowStore,
|
|
||||||
} from '../store'
|
} from '../store'
|
||||||
import { BlockEnum } from '../types'
|
|
||||||
import { useWorkflowUpdate } from '../hooks'
|
|
||||||
import {
|
import {
|
||||||
useNodesReadOnly,
|
useNodesReadOnly,
|
||||||
} from './use-workflow'
|
} from './use-workflow'
|
||||||
import { syncWorkflowDraft } from '@/service/workflow'
|
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
|
||||||
import { API_PREFIX } from '@/config'
|
|
||||||
|
|
||||||
export const useNodesSyncDraft = () => {
|
export const useNodesSyncDraft = () => {
|
||||||
const store = useStoreApi()
|
|
||||||
const workflowStore = useWorkflowStore()
|
|
||||||
const featuresStore = useFeaturesStore()
|
|
||||||
const { getNodesReadOnly } = useNodesReadOnly()
|
const { getNodesReadOnly } = useNodesReadOnly()
|
||||||
const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
|
|
||||||
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
|
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
|
||||||
const params = useParams()
|
const doSyncWorkflowDraft = useHooksStore(s => s.doSyncWorkflowDraft)
|
||||||
|
const syncWorkflowDraftWhenPageClose = useHooksStore(s => s.syncWorkflowDraftWhenPageClose)
|
||||||
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 handleSyncWorkflowDraft = useCallback((
|
const handleSyncWorkflowDraft = useCallback((
|
||||||
sync?: boolean,
|
sync?: boolean,
|
||||||
|
|
|
@ -25,8 +25,8 @@ import {
|
||||||
useSelectionInteractions,
|
useSelectionInteractions,
|
||||||
useWorkflowReadOnly,
|
useWorkflowReadOnly,
|
||||||
} from '../hooks'
|
} from '../hooks'
|
||||||
import { useEdgesInteractions } from './use-edges-interactions'
|
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
|
||||||
import { useNodesInteractions } from './use-nodes-interactions'
|
import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
|
||||||
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
import { useNodesSyncDraft } from './use-nodes-sync-draft'
|
||||||
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
|
@ -37,8 +37,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
|
|
||||||
export const useWorkflowInteractions = () => {
|
export const useWorkflowInteractions = () => {
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const { handleNodeCancelRunningStatus } = useNodesInteractions()
|
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
|
||||||
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
|
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
|
||||||
|
|
||||||
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
const handleCancelDebugAndPreviewPanel = useCallback(() => {
|
||||||
workflowStore.setState({
|
workflowStore.setState({
|
||||||
|
|
|
@ -1,350 +1,11 @@
|
||||||
import { useCallback } from 'react'
|
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||||
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'
|
|
||||||
|
|
||||||
export const useWorkflowRun = () => {
|
export const useWorkflowRun = () => {
|
||||||
const store = useStoreApi()
|
const handleBackupDraft = useHooksStore(s => s.handleBackupDraft)
|
||||||
const workflowStore = useWorkflowStore()
|
const handleLoadBackupDraft = useHooksStore(s => s.handleLoadBackupDraft)
|
||||||
const reactflow = useReactFlow()
|
const handleRestoreFromPublishedWorkflow = useHooksStore(s => s.handleRestoreFromPublishedWorkflow)
|
||||||
const featuresStore = useFeaturesStore()
|
const handleRun = useHooksStore(s => s.handleRun)
|
||||||
const { doSyncWorkflowDraft } = useNodesSyncDraft()
|
const handleStopRun = useHooksStore(s => s.handleStopRun)
|
||||||
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 {
|
return {
|
||||||
handleBackupDraft,
|
handleBackupDraft,
|
||||||
|
|
|
@ -1,92 +1,9 @@
|
||||||
import { useCallback } from 'react'
|
import { useHooksStore } from '@/app/components/workflow/hooks-store'
|
||||||
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'
|
|
||||||
|
|
||||||
export const useWorkflowStartRun = () => {
|
export const useWorkflowStartRun = () => {
|
||||||
const store = useStoreApi()
|
const handleStartWorkflowRun = useHooksStore(s => s.handleStartWorkflowRun)
|
||||||
const workflowStore = useWorkflowStore()
|
const handleWorkflowStartRunInWorkflow = useHooksStore(s => s.handleWorkflowStartRunInWorkflow)
|
||||||
const featuresStore = useFeaturesStore()
|
const handleWorkflowStartRunInChatflow = useHooksStore(s => s.handleWorkflowStartRunInChatflow)
|
||||||
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 {
|
return {
|
||||||
handleStartWorkflowRun,
|
handleStartWorkflowRun,
|
||||||
|
|
|
@ -1,13 +1,9 @@
|
||||||
import {
|
import {
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
|
||||||
useMemo,
|
useMemo,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import dayjs from 'dayjs'
|
|
||||||
import { uniqBy } from 'lodash-es'
|
import { uniqBy } from 'lodash-es'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
import { useContext } from 'use-context-selector'
|
|
||||||
import {
|
import {
|
||||||
getIncomers,
|
getIncomers,
|
||||||
getOutgoers,
|
getOutgoers,
|
||||||
|
@ -40,25 +36,15 @@ import {
|
||||||
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
|
||||||
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
|
||||||
import { useNodesExtraData } from './use-nodes-data'
|
import { useNodesExtraData } from './use-nodes-data'
|
||||||
import { useWorkflowTemplate } from './use-workflow-template'
|
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
import { useStore as useAppStore } from '@/app/components/app/store'
|
||||||
import {
|
|
||||||
fetchNodesDefaultConfigs,
|
|
||||||
fetchPublishedWorkflow,
|
|
||||||
fetchWorkflowDraft,
|
|
||||||
syncWorkflowDraft,
|
|
||||||
} from '@/service/workflow'
|
|
||||||
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
|
|
||||||
import {
|
import {
|
||||||
fetchAllBuiltInTools,
|
fetchAllBuiltInTools,
|
||||||
fetchAllCustomTools,
|
fetchAllCustomTools,
|
||||||
fetchAllWorkflowTools,
|
fetchAllWorkflowTools,
|
||||||
} from '@/service/tools'
|
} from '@/service/tools'
|
||||||
import I18n from '@/context/i18n'
|
|
||||||
import { CollectionType } from '@/app/components/tools/types'
|
import { CollectionType } from '@/app/components/tools/types'
|
||||||
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
|
||||||
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
|
||||||
import { useWorkflowConfig } from '@/service/use-workflow'
|
|
||||||
import { basePath } from '@/utils/var'
|
import { basePath } from '@/utils/var'
|
||||||
import { canFindTool } from '@/utils'
|
import { canFindTool } from '@/utils'
|
||||||
|
|
||||||
|
@ -70,12 +56,9 @@ export const useIsChatMode = () => {
|
||||||
|
|
||||||
export const useWorkflow = () => {
|
export const useWorkflow = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
const { locale } = useContext(I18n)
|
|
||||||
const store = useStoreApi()
|
const store = useStoreApi()
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const appId = useStore(s => s.appId)
|
|
||||||
const nodesExtraData = useNodesExtraData()
|
const nodesExtraData = useNodesExtraData()
|
||||||
const { data: workflowConfig } = useWorkflowConfig(appId)
|
|
||||||
const setPanelWidth = useCallback((width: number) => {
|
const setPanelWidth = useCallback((width: number) => {
|
||||||
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
localStorage.setItem('workflow-node-panel-width', `${width}`)
|
||||||
workflowStore.setState({ panelWidth: width })
|
workflowStore.setState({ panelWidth: width })
|
||||||
|
@ -344,6 +327,7 @@ export const useWorkflow = () => {
|
||||||
parallelList,
|
parallelList,
|
||||||
hasAbnormalEdges,
|
hasAbnormalEdges,
|
||||||
} = getParallelInfo(nodes, edges, parentNodeId)
|
} = getParallelInfo(nodes, edges, parentNodeId)
|
||||||
|
const { workflowConfig } = workflowStore.getState()
|
||||||
|
|
||||||
if (hasAbnormalEdges)
|
if (hasAbnormalEdges)
|
||||||
return false
|
return false
|
||||||
|
@ -359,7 +343,7 @@ export const useWorkflow = () => {
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}, [t, workflowStore, workflowConfig?.parallel_depth_limit])
|
}, [t, workflowStore])
|
||||||
|
|
||||||
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
|
||||||
const {
|
const {
|
||||||
|
@ -407,10 +391,6 @@ export const useWorkflow = () => {
|
||||||
return !hasCycle(targetNode)
|
return !hasCycle(targetNode)
|
||||||
}, [store, nodesExtraData, checkParallelLimit])
|
}, [store, nodesExtraData, checkParallelLimit])
|
||||||
|
|
||||||
const formatTimeFromNow = useCallback((time: number) => {
|
|
||||||
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
|
|
||||||
}, [locale])
|
|
||||||
|
|
||||||
const getNode = useCallback((nodeId?: string) => {
|
const getNode = useCallback((nodeId?: string) => {
|
||||||
const { getNodes } = store.getState()
|
const { getNodes } = store.getState()
|
||||||
const nodes = getNodes()
|
const nodes = getNodes()
|
||||||
|
@ -432,7 +412,6 @@ export const useWorkflow = () => {
|
||||||
checkNestedParallelLimit,
|
checkNestedParallelLimit,
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
isFromStartNode,
|
isFromStartNode,
|
||||||
formatTimeFromNow,
|
|
||||||
getNode,
|
getNode,
|
||||||
getBeforeNodeById,
|
getBeforeNodeById,
|
||||||
getIterationNodeChildren,
|
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 = () => {
|
export const useWorkflowReadOnly = () => {
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const workflowRunningData = useStore(s => s.workflowRunningData)
|
const workflowRunningData = useStore(s => s.workflowRunningData)
|
||||||
|
|
|
@ -5,11 +5,8 @@ import {
|
||||||
memo,
|
memo,
|
||||||
useCallback,
|
useCallback,
|
||||||
useEffect,
|
useEffect,
|
||||||
useMemo,
|
|
||||||
useRef,
|
useRef,
|
||||||
useState,
|
|
||||||
} from 'react'
|
} from 'react'
|
||||||
import useSWR from 'swr'
|
|
||||||
import { setAutoFreeze } from 'immer'
|
import { setAutoFreeze } from 'immer'
|
||||||
import {
|
import {
|
||||||
useEventListener,
|
useEventListener,
|
||||||
|
@ -31,17 +28,14 @@ import 'reactflow/dist/style.css'
|
||||||
import './style.css'
|
import './style.css'
|
||||||
import type {
|
import type {
|
||||||
Edge,
|
Edge,
|
||||||
EnvironmentVariable,
|
|
||||||
Node,
|
Node,
|
||||||
} from './types'
|
} from './types'
|
||||||
import {
|
import {
|
||||||
ControlMode,
|
ControlMode,
|
||||||
SupportUploadFileTypes,
|
|
||||||
} from './types'
|
} from './types'
|
||||||
import { WorkflowContextProvider } from './context'
|
|
||||||
import {
|
import {
|
||||||
useDSL,
|
|
||||||
useEdgesInteractions,
|
useEdgesInteractions,
|
||||||
|
useFetchToolsData,
|
||||||
useNodesInteractions,
|
useNodesInteractions,
|
||||||
useNodesReadOnly,
|
useNodesReadOnly,
|
||||||
useNodesSyncDraft,
|
useNodesSyncDraft,
|
||||||
|
@ -49,11 +43,9 @@ import {
|
||||||
useSelectionInteractions,
|
useSelectionInteractions,
|
||||||
useShortcuts,
|
useShortcuts,
|
||||||
useWorkflow,
|
useWorkflow,
|
||||||
useWorkflowInit,
|
|
||||||
useWorkflowReadOnly,
|
useWorkflowReadOnly,
|
||||||
useWorkflowUpdate,
|
useWorkflowUpdate,
|
||||||
} from './hooks'
|
} from './hooks'
|
||||||
import Header from './header'
|
|
||||||
import CustomNode from './nodes'
|
import CustomNode from './nodes'
|
||||||
import CustomNoteNode from './note-node'
|
import CustomNoteNode from './note-node'
|
||||||
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
import { CUSTOM_NOTE_NODE } from './note-node/constants'
|
||||||
|
@ -66,42 +58,28 @@ import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
|
||||||
import Operator from './operator'
|
import Operator from './operator'
|
||||||
import CustomEdge from './custom-edge'
|
import CustomEdge from './custom-edge'
|
||||||
import CustomConnectionLine from './custom-connection-line'
|
import CustomConnectionLine from './custom-connection-line'
|
||||||
import Panel from './panel'
|
|
||||||
import Features from './features'
|
|
||||||
import HelpLine from './help-line'
|
import HelpLine from './help-line'
|
||||||
import CandidateNode from './candidate-node'
|
import CandidateNode from './candidate-node'
|
||||||
import PanelContextmenu from './panel-contextmenu'
|
import PanelContextmenu from './panel-contextmenu'
|
||||||
import NodeContextmenu from './node-contextmenu'
|
import NodeContextmenu from './node-contextmenu'
|
||||||
import SyncingDataModal from './syncing-data-modal'
|
import SyncingDataModal from './syncing-data-modal'
|
||||||
import UpdateDSLModal from './update-dsl-modal'
|
|
||||||
import DSLExportConfirmModal from './dsl-export-confirm-modal'
|
|
||||||
import LimitTips from './limit-tips'
|
import LimitTips from './limit-tips'
|
||||||
import PluginDependency from './plugin-dependency'
|
|
||||||
import {
|
import {
|
||||||
useStore,
|
useStore,
|
||||||
useWorkflowStore,
|
useWorkflowStore,
|
||||||
} from './store'
|
} from './store'
|
||||||
import {
|
|
||||||
initialEdges,
|
|
||||||
initialNodes,
|
|
||||||
} from './utils'
|
|
||||||
import {
|
import {
|
||||||
CUSTOM_EDGE,
|
CUSTOM_EDGE,
|
||||||
CUSTOM_NODE,
|
CUSTOM_NODE,
|
||||||
DSL_EXPORT_CHECK,
|
|
||||||
ITERATION_CHILDREN_Z_INDEX,
|
ITERATION_CHILDREN_Z_INDEX,
|
||||||
WORKFLOW_DATA_UPDATE,
|
WORKFLOW_DATA_UPDATE,
|
||||||
} from './constants'
|
} from './constants'
|
||||||
import { WorkflowHistoryProvider } from './workflow-history-store'
|
import { WorkflowHistoryProvider } from './workflow-history-store'
|
||||||
import Loading from '@/app/components/base/loading'
|
|
||||||
import { FeaturesProvider } from '@/app/components/base/features'
|
|
||||||
import type { Features as FeaturesData } from '@/app/components/base/features/types'
|
|
||||||
import { useFeaturesStore } from '@/app/components/base/features/hooks'
|
|
||||||
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
import { useEventEmitterContextContext } from '@/context/event-emitter'
|
||||||
import Confirm from '@/app/components/base/confirm'
|
import Confirm from '@/app/components/base/confirm'
|
||||||
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
|
|
||||||
import { fetchFileUploadConfig } from '@/service/common'
|
|
||||||
import DatasetsDetailProvider from './datasets-detail-store/provider'
|
import DatasetsDetailProvider from './datasets-detail-store/provider'
|
||||||
|
import { HooksStoreContextProvider } from './hooks-store'
|
||||||
|
import type { Shape as HooksStoreShape } from './hooks-store'
|
||||||
|
|
||||||
const nodeTypes = {
|
const nodeTypes = {
|
||||||
[CUSTOM_NODE]: CustomNode,
|
[CUSTOM_NODE]: CustomNode,
|
||||||
|
@ -114,32 +92,32 @@ const edgeTypes = {
|
||||||
[CUSTOM_EDGE]: CustomEdge,
|
[CUSTOM_EDGE]: CustomEdge,
|
||||||
}
|
}
|
||||||
|
|
||||||
type WorkflowProps = {
|
export type WorkflowProps = {
|
||||||
nodes: Node[]
|
nodes: Node[]
|
||||||
edges: Edge[]
|
edges: Edge[]
|
||||||
viewport?: Viewport
|
viewport?: Viewport
|
||||||
|
children?: React.ReactNode
|
||||||
|
onWorkflowDataUpdate?: (v: any) => void
|
||||||
}
|
}
|
||||||
const Workflow: FC<WorkflowProps> = memo(({
|
export const Workflow: FC<WorkflowProps> = memo(({
|
||||||
nodes: originalNodes,
|
nodes: originalNodes,
|
||||||
edges: originalEdges,
|
edges: originalEdges,
|
||||||
viewport,
|
viewport,
|
||||||
|
children,
|
||||||
|
onWorkflowDataUpdate,
|
||||||
}) => {
|
}) => {
|
||||||
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
const workflowContainerRef = useRef<HTMLDivElement>(null)
|
||||||
const workflowStore = useWorkflowStore()
|
const workflowStore = useWorkflowStore()
|
||||||
const reactflow = useReactFlow()
|
const reactflow = useReactFlow()
|
||||||
const featuresStore = useFeaturesStore()
|
|
||||||
const [nodes, setNodes] = useNodesState(originalNodes)
|
const [nodes, setNodes] = useNodesState(originalNodes)
|
||||||
const [edges, setEdges] = useEdgesState(originalEdges)
|
const [edges, setEdges] = useEdgesState(originalEdges)
|
||||||
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
|
|
||||||
const controlMode = useStore(s => s.controlMode)
|
const controlMode = useStore(s => s.controlMode)
|
||||||
const nodeAnimation = useStore(s => s.nodeAnimation)
|
const nodeAnimation = useStore(s => s.nodeAnimation)
|
||||||
const showConfirm = useStore(s => s.showConfirm)
|
const showConfirm = useStore(s => s.showConfirm)
|
||||||
const showImportDSLModal = useStore(s => s.showImportDSLModal)
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
setShowConfirm,
|
setShowConfirm,
|
||||||
setControlPromptEditorRerenderKey,
|
setControlPromptEditorRerenderKey,
|
||||||
setShowImportDSLModal,
|
|
||||||
setSyncWorkflowDraftHash,
|
setSyncWorkflowDraftHash,
|
||||||
} = workflowStore.getState()
|
} = workflowStore.getState()
|
||||||
const {
|
const {
|
||||||
|
@ -148,12 +126,10 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
} = useNodesSyncDraft()
|
} = useNodesSyncDraft()
|
||||||
const { workflowReadOnly } = useWorkflowReadOnly()
|
const { workflowReadOnly } = useWorkflowReadOnly()
|
||||||
const { nodesReadOnly } = useNodesReadOnly()
|
const { nodesReadOnly } = useNodesReadOnly()
|
||||||
|
|
||||||
const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])
|
|
||||||
|
|
||||||
const { eventEmitter } = useEventEmitterContextContext()
|
const { eventEmitter } = useEventEmitterContextContext()
|
||||||
|
|
||||||
eventEmitter?.useSubscription((v: any) => {
|
eventEmitter?.useSubscription((v: any) => {
|
||||||
|
console.log(v, 'eventEmitter')
|
||||||
if (v.type === WORKFLOW_DATA_UPDATE) {
|
if (v.type === WORKFLOW_DATA_UPDATE) {
|
||||||
setNodes(v.payload.nodes)
|
setNodes(v.payload.nodes)
|
||||||
setEdges(v.payload.edges)
|
setEdges(v.payload.edges)
|
||||||
|
@ -161,19 +137,13 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
if (v.payload.viewport)
|
if (v.payload.viewport)
|
||||||
reactflow.setViewport(v.payload.viewport)
|
reactflow.setViewport(v.payload.viewport)
|
||||||
|
|
||||||
if (v.payload.features && featuresStore) {
|
|
||||||
const { setFeatures } = featuresStore.getState()
|
|
||||||
|
|
||||||
setFeatures(v.payload.features)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (v.payload.hash)
|
if (v.payload.hash)
|
||||||
setSyncWorkflowDraftHash(v.payload.hash)
|
setSyncWorkflowDraftHash(v.payload.hash)
|
||||||
|
|
||||||
|
onWorkflowDataUpdate?.(v.payload)
|
||||||
|
|
||||||
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
|
||||||
}
|
}
|
||||||
if (v.type === DSL_EXPORT_CHECK)
|
|
||||||
setSecretEnvList(v.payload.data as EnvironmentVariable[])
|
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
@ -231,6 +201,12 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
const { handleFetchAllTools } = useFetchToolsData()
|
||||||
|
useEffect(() => {
|
||||||
|
handleFetchAllTools('builtin')
|
||||||
|
handleFetchAllTools('custom')
|
||||||
|
handleFetchAllTools('workflow')
|
||||||
|
}, [handleFetchAllTools])
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleNodeDragStart,
|
handleNodeDragStart,
|
||||||
|
@ -258,15 +234,10 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
} = useSelectionInteractions()
|
} = useSelectionInteractions()
|
||||||
const {
|
const {
|
||||||
handlePaneContextMenu,
|
handlePaneContextMenu,
|
||||||
handlePaneContextmenuCancel,
|
|
||||||
} = usePanelInteractions()
|
} = usePanelInteractions()
|
||||||
const {
|
const {
|
||||||
isValidConnection,
|
isValidConnection,
|
||||||
} = useWorkflow()
|
} = useWorkflow()
|
||||||
const {
|
|
||||||
exportCheck,
|
|
||||||
handleExportDSL,
|
|
||||||
} = useDSL()
|
|
||||||
|
|
||||||
useOnViewportChange({
|
useOnViewportChange({
|
||||||
onEnd: () => {
|
onEnd: () => {
|
||||||
|
@ -297,12 +268,7 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
>
|
>
|
||||||
<SyncingDataModal />
|
<SyncingDataModal />
|
||||||
<CandidateNode />
|
<CandidateNode />
|
||||||
<Header />
|
|
||||||
<Panel />
|
|
||||||
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
|
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
|
||||||
{
|
|
||||||
showFeaturesPanel && <Features />
|
|
||||||
}
|
|
||||||
<PanelContextmenu />
|
<PanelContextmenu />
|
||||||
<NodeContextmenu />
|
<NodeContextmenu />
|
||||||
<HelpLine />
|
<HelpLine />
|
||||||
|
@ -317,26 +283,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 />
|
<LimitTips />
|
||||||
<PluginDependency />
|
{children}
|
||||||
<ReactFlow
|
<ReactFlow
|
||||||
nodeTypes={nodeTypes}
|
nodeTypes={nodeTypes}
|
||||||
edgeTypes={edgeTypes}
|
edgeTypes={edgeTypes}
|
||||||
|
@ -389,89 +337,43 @@ const Workflow: FC<WorkflowProps> = memo(({
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
Workflow.displayName = 'Workflow'
|
|
||||||
|
|
||||||
const WorkflowWrap = memo(() => {
|
type WorkflowWithInnerContextProps = WorkflowProps & {
|
||||||
const {
|
hooksStore?: Partial<HooksStoreShape>
|
||||||
data,
|
}
|
||||||
isLoading,
|
export const WorkflowWithInnerContext = memo(({
|
||||||
} = useWorkflowInit()
|
hooksStore,
|
||||||
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)
|
...restProps
|
||||||
|
}: WorkflowWithInnerContextProps) => {
|
||||||
|
return (
|
||||||
|
<HooksStoreContextProvider {...hooksStore}>
|
||||||
|
<Workflow {...restProps} />
|
||||||
|
</HooksStoreContextProvider>
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
const nodesData = useMemo(() => {
|
type WorkflowWithDefaultContextProps =
|
||||||
if (data)
|
Pick<WorkflowProps, 'edges' | 'nodes'>
|
||||||
return initialNodes(data.graph.nodes, data.graph.edges)
|
& {
|
||||||
|
children: React.ReactNode
|
||||||
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 },
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const WorkflowWithDefaultContext = ({
|
||||||
|
nodes,
|
||||||
|
edges,
|
||||||
|
children,
|
||||||
|
}: WorkflowWithDefaultContextProps) => {
|
||||||
return (
|
return (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<WorkflowHistoryProvider
|
<WorkflowHistoryProvider
|
||||||
nodes={nodesData}
|
nodes={nodes}
|
||||||
edges={edgesData} >
|
edges={edges} >
|
||||||
<FeaturesProvider features={initialFeatures}>
|
<DatasetsDetailProvider nodes={nodes}>
|
||||||
<DatasetsDetailProvider nodes={nodesData}>
|
{children}
|
||||||
<Workflow
|
</DatasetsDetailProvider>
|
||||||
nodes={nodesData}
|
|
||||||
edges={edgesData}
|
|
||||||
viewport={data?.graph.viewport}
|
|
||||||
/>
|
|
||||||
</DatasetsDetailProvider>
|
|
||||||
</FeaturesProvider>
|
|
||||||
</WorkflowHistoryProvider>
|
</WorkflowHistoryProvider>
|
||||||
</ReactFlowProvider>
|
</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 type { FC } from 'react'
|
||||||
import { memo } from 'react'
|
import { memo } from 'react'
|
||||||
import { useNodes } from 'reactflow'
|
import { useNodes } from 'reactflow'
|
||||||
import { useShallow } from 'zustand/react/shallow'
|
|
||||||
import type { CommonNodeType } from '../types'
|
import type { CommonNodeType } from '../types'
|
||||||
import { Panel as NodePanel } from '../nodes'
|
import { Panel as NodePanel } from '../nodes'
|
||||||
import { useStore } from '../store'
|
import { useStore } from '../store'
|
||||||
import {
|
|
||||||
useIsChatMode,
|
|
||||||
} from '../hooks'
|
|
||||||
import DebugAndPreview from './debug-and-preview'
|
|
||||||
import Record from './record'
|
|
||||||
import WorkflowPreview from './workflow-preview'
|
|
||||||
import ChatRecord from './chat-record'
|
|
||||||
import ChatVariablePanel from './chat-variable-panel'
|
|
||||||
import EnvPanel from './env-panel'
|
import EnvPanel from './env-panel'
|
||||||
import GlobalVariablePanel from './global-variable-panel'
|
|
||||||
import VersionHistoryPanel from './version-history-panel'
|
|
||||||
import cn from '@/utils/classnames'
|
import cn from '@/utils/classnames'
|
||||||
import { useStore as useAppStore } from '@/app/components/app/store'
|
|
||||||
import MessageLogModal from '@/app/components/base/message-log-modal'
|
|
||||||
|
|
||||||
const Panel: FC = () => {
|
export type PanelProps = {
|
||||||
|
components?: {
|
||||||
|
left?: React.ReactNode
|
||||||
|
right?: React.ReactNode
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const Panel: FC<PanelProps> = ({
|
||||||
|
components,
|
||||||
|
}) => {
|
||||||
const nodes = useNodes<CommonNodeType>()
|
const nodes = useNodes<CommonNodeType>()
|
||||||
const isChatMode = useIsChatMode()
|
|
||||||
const selectedNode = nodes.find(node => node.data.selected)
|
const selectedNode = nodes.find(node => node.data.selected)
|
||||||
const historyWorkflowData = useStore(s => s.historyWorkflowData)
|
|
||||||
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
|
|
||||||
const showEnvPanel = useStore(s => s.showEnvPanel)
|
const showEnvPanel = useStore(s => s.showEnvPanel)
|
||||||
const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
|
|
||||||
const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel)
|
|
||||||
const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel)
|
|
||||||
const isRestoring = useStore(s => s.isRestoring)
|
const isRestoring = useStore(s => s.isRestoring)
|
||||||
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
|
|
||||||
currentLogItem: state.currentLogItem,
|
|
||||||
setCurrentLogItem: state.setCurrentLogItem,
|
|
||||||
showMessageLogModal: state.showMessageLogModal,
|
|
||||||
setShowMessageLogModal: state.setShowMessageLogModal,
|
|
||||||
currentLogModalActiveTab: state.currentLogModalActiveTab,
|
|
||||||
})))
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
@ -46,18 +28,7 @@ const Panel: FC = () => {
|
||||||
key={`${isRestoring}`}
|
key={`${isRestoring}`}
|
||||||
>
|
>
|
||||||
{
|
{
|
||||||
showMessageLogModal && (
|
components?.left
|
||||||
<MessageLogModal
|
|
||||||
fixedWidth
|
|
||||||
width={400}
|
|
||||||
currentLogItem={currentLogItem}
|
|
||||||
onCancel={() => {
|
|
||||||
setCurrentLogItem()
|
|
||||||
setShowMessageLogModal(false)
|
|
||||||
}}
|
|
||||||
defaultTab={currentLogModalActiveTab}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
!!selectedNode && (
|
!!selectedNode && (
|
||||||
|
@ -65,23 +36,11 @@ const Panel: FC = () => {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
historyWorkflowData && !isChatMode && (
|
components?.right
|
||||||
<Record />
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
historyWorkflowData && isChatMode && (
|
isRestoring && (
|
||||||
<ChatRecord />
|
<div className='flex-1' />
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
showDebugAndPreviewPanel && isChatMode && (
|
|
||||||
<DebugAndPreview />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
showDebugAndPreviewPanel && !isChatMode && (
|
|
||||||
<WorkflowPreview />
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -89,21 +48,6 @@ const Panel: FC = () => {
|
||||||
<EnvPanel />
|
<EnvPanel />
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
{
|
|
||||||
showChatVariablePanel && (
|
|
||||||
<ChatVariablePanel />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
showGlobalVariablePanel && (
|
|
||||||
<GlobalVariablePanel />
|
|
||||||
)
|
|
||||||
}
|
|
||||||
{
|
|
||||||
showWorkflowVersionHistoryPanel && (
|
|
||||||
<VersionHistoryPanel/>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,7 @@
|
||||||
import { useContext } from 'react'
|
import { useContext } from 'react'
|
||||||
|
import type {
|
||||||
|
StateCreator,
|
||||||
|
} from 'zustand'
|
||||||
import {
|
import {
|
||||||
useStore as useZustandStore,
|
useStore as useZustandStore,
|
||||||
} from 'zustand'
|
} from 'zustand'
|
||||||
|
@ -26,6 +29,7 @@ import { createWorkflowDraftSlice } from './workflow-draft-slice'
|
||||||
import type { WorkflowSliceShape } from './workflow-slice'
|
import type { WorkflowSliceShape } from './workflow-slice'
|
||||||
import { createWorkflowSlice } from './workflow-slice'
|
import { createWorkflowSlice } from './workflow-slice'
|
||||||
import { WorkflowContext } from '@/app/components/workflow/context'
|
import { WorkflowContext } from '@/app/components/workflow/context'
|
||||||
|
import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice'
|
||||||
|
|
||||||
export type Shape =
|
export type Shape =
|
||||||
ChatVariableSliceShape &
|
ChatVariableSliceShape &
|
||||||
|
@ -38,9 +42,16 @@ export type Shape =
|
||||||
ToolSliceShape &
|
ToolSliceShape &
|
||||||
VersionSliceShape &
|
VersionSliceShape &
|
||||||
WorkflowDraftSliceShape &
|
WorkflowDraftSliceShape &
|
||||||
WorkflowSliceShape
|
WorkflowSliceShape &
|
||||||
|
WorkflowAppSliceShape
|
||||||
|
|
||||||
|
type CreateWorkflowStoreParams = {
|
||||||
|
injectWorkflowStoreSliceFn?: StateCreator<WorkflowAppSliceShape>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
|
||||||
|
const { injectWorkflowStoreSliceFn } = params || {}
|
||||||
|
|
||||||
export const createWorkflowStore = () => {
|
|
||||||
return createStore<Shape>((...args) => ({
|
return createStore<Shape>((...args) => ({
|
||||||
...createChatVariableSlice(...args),
|
...createChatVariableSlice(...args),
|
||||||
...createEnvVariableSlice(...args),
|
...createEnvVariableSlice(...args),
|
||||||
|
@ -53,6 +64,7 @@ export const createWorkflowStore = () => {
|
||||||
...createVersionSlice(...args),
|
...createVersionSlice(...args),
|
||||||
...createWorkflowDraftSlice(...args),
|
...createWorkflowDraftSlice(...args),
|
||||||
...createWorkflowSlice(...args),
|
...createWorkflowSlice(...args),
|
||||||
|
...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape),
|
||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -12,8 +12,6 @@ import type {
|
||||||
export type NodeSliceShape = {
|
export type NodeSliceShape = {
|
||||||
showSingleRunPanel: boolean
|
showSingleRunPanel: boolean
|
||||||
setShowSingleRunPanel: (showSingleRunPanel: boolean) => void
|
setShowSingleRunPanel: (showSingleRunPanel: boolean) => void
|
||||||
nodesDefaultConfigs: Record<string, any>
|
|
||||||
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
|
|
||||||
nodeAnimation: boolean
|
nodeAnimation: boolean
|
||||||
setNodeAnimation: (nodeAnimation: boolean) => void
|
setNodeAnimation: (nodeAnimation: boolean) => void
|
||||||
candidateNode?: Node
|
candidateNode?: Node
|
||||||
|
@ -55,8 +53,6 @@ export type NodeSliceShape = {
|
||||||
export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({
|
export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({
|
||||||
showSingleRunPanel: false,
|
showSingleRunPanel: false,
|
||||||
setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })),
|
setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })),
|
||||||
nodesDefaultConfigs: {},
|
|
||||||
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
|
|
||||||
nodeAnimation: false,
|
nodeAnimation: false,
|
||||||
setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })),
|
setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })),
|
||||||
candidateNode: undefined,
|
candidateNode: undefined,
|
||||||
|
|
|
@ -10,11 +10,8 @@ type PreviewRunningData = WorkflowRunningData & {
|
||||||
}
|
}
|
||||||
|
|
||||||
export type WorkflowSliceShape = {
|
export type WorkflowSliceShape = {
|
||||||
appId: string
|
|
||||||
workflowRunningData?: PreviewRunningData
|
workflowRunningData?: PreviewRunningData
|
||||||
setWorkflowRunningData: (workflowData: PreviewRunningData) => void
|
setWorkflowRunningData: (workflowData: PreviewRunningData) => void
|
||||||
notInitialWorkflow: boolean
|
|
||||||
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
|
|
||||||
clipboardElements: Node[]
|
clipboardElements: Node[]
|
||||||
setClipboardElements: (clipboardElements: Node[]) => void
|
setClipboardElements: (clipboardElements: Node[]) => void
|
||||||
selection: null | { x1: number; y1: number; x2: number; y2: number }
|
selection: null | { x1: number; y1: number; x2: number; y2: number }
|
||||||
|
@ -33,14 +30,13 @@ export type WorkflowSliceShape = {
|
||||||
setShowImportDSLModal: (showImportDSLModal: boolean) => void
|
setShowImportDSLModal: (showImportDSLModal: boolean) => void
|
||||||
showTips: string
|
showTips: string
|
||||||
setShowTips: (showTips: string) => void
|
setShowTips: (showTips: string) => void
|
||||||
|
workflowConfig?: Record<string, any>
|
||||||
|
setWorkflowConfig: (workflowConfig: Record<string, any>) => void
|
||||||
}
|
}
|
||||||
|
|
||||||
export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
||||||
appId: '',
|
|
||||||
workflowRunningData: undefined,
|
workflowRunningData: undefined,
|
||||||
setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })),
|
setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })),
|
||||||
notInitialWorkflow: false,
|
|
||||||
setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })),
|
|
||||||
clipboardElements: [],
|
clipboardElements: [],
|
||||||
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
|
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
|
||||||
selection: null,
|
selection: null,
|
||||||
|
@ -62,4 +58,6 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
|
||||||
setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })),
|
setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })),
|
||||||
showTips: '',
|
showTips: '',
|
||||||
setShowTips: showTips => set(() => ({ 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({
|
return useQuery({
|
||||||
queryKey: [NAME_SPACE, 'config', appId],
|
queryKey: [NAME_SPACE, 'config', appId],
|
||||||
queryFn: () => get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`),
|
queryFn: async () => {
|
||||||
|
const data = await get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`)
|
||||||
|
onSuccess(data)
|
||||||
|
return data
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue