Chore/slice workflow (#18351)

This commit is contained in:
zxhlyh
2025-04-18 13:59:12 +08:00
committed by GitHub
parent 523efbfea5
commit efe5db38ee
44 changed files with 1855 additions and 1239 deletions

View File

@@ -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)

View File

@@ -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)

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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'

View File

@@ -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'
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -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,
}
}

View File

@@ -0,0 +1,74 @@
import { generateNewNode } from '@/app/components/workflow/utils'
import {
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
} from '@/app/components/workflow/constants'
import { useNodesInitialData } from '@/app/components/workflow/hooks'
import { useIsChatMode } from './use-is-chat-mode'
export const useWorkflowTemplate = () => {
const isChatMode = useIsChatMode()
const nodesInitialData = useNodesInitialData()
const { newNode: startNode } = generateNewNode({
data: nodesInitialData.start,
position: START_INITIAL_POSITION,
})
if (isChatMode) {
const { newNode: llmNode } = generateNewNode({
id: 'llm',
data: {
...nodesInitialData.llm,
memory: {
window: { enabled: false, size: 10 },
query_prompt_template: '{{#sys.query#}}',
},
selected: true,
},
position: {
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET,
y: START_INITIAL_POSITION.y,
},
} as any)
const { newNode: answerNode } = generateNewNode({
id: 'answer',
data: {
...nodesInitialData.answer,
answer: `{{#${llmNode.id}.text#}}`,
},
position: {
x: START_INITIAL_POSITION.x + NODE_WIDTH_X_OFFSET * 2,
y: START_INITIAL_POSITION.y,
},
} as any)
const startToLlmEdge = {
id: `${startNode.id}-${llmNode.id}`,
source: startNode.id,
sourceHandle: 'source',
target: llmNode.id,
targetHandle: 'target',
}
const llmToAnswerEdge = {
id: `${llmNode.id}-${answerNode.id}`,
source: llmNode.id,
sourceHandle: 'source',
target: answerNode.id,
targetHandle: 'target',
}
return {
nodes: [startNode, llmNode, answerNode],
edges: [startToLlmEdge, llmToAnswerEdge],
}
}
else {
return {
nodes: [startNode],
edges: [],
}
}
}

View File

@@ -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

View File

@@ -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 })),
})