Ver código fonte

Chore/slice workflow (#18351)

tags/1.3.0
zxhlyh 6 meses atrás
pai
commit
efe5db38ee
Nenhuma conta vinculada ao e-mail do autor do commit
44 arquivos alterados com 1856 adições e 1240 exclusões
  1. 2
    2
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx
  2. 69
    0
      web/app/components/workflow-app/components/workflow-children.tsx
  3. 11
    0
      web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.tsx
  4. 152
    0
      web/app/components/workflow-app/components/workflow-header/features-trigger.tsx
  5. 31
    0
      web/app/components/workflow-app/components/workflow-header/index.tsx
  6. 87
    0
      web/app/components/workflow-app/components/workflow-main.tsx
  7. 109
    0
      web/app/components/workflow-app/components/workflow-panel.tsx
  8. 6
    0
      web/app/components/workflow-app/hooks/index.ts
  9. 7
    0
      web/app/components/workflow-app/hooks/use-is-chat-mode.ts
  10. 148
    0
      web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts
  11. 123
    0
      web/app/components/workflow-app/hooks/use-workflow-init.ts
  12. 357
    0
      web/app/components/workflow-app/hooks/use-workflow-run.ts
  13. 96
    0
      web/app/components/workflow-app/hooks/use-workflow-start-run.tsx
  14. 4
    4
      web/app/components/workflow-app/hooks/use-workflow-template.ts
  15. 108
    0
      web/app/components/workflow-app/index.tsx
  16. 18
    0
      web/app/components/workflow-app/store/workflow/workflow-slice.ts
  17. 9
    4
      web/app/components/workflow/context.tsx
  18. 2
    2
      web/app/components/workflow/header/editing-title.tsx
  19. 69
    0
      web/app/components/workflow/header/header-in-normal.tsx
  20. 93
    0
      web/app/components/workflow/header/header-in-restoring.tsx
  21. 50
    0
      web/app/components/workflow/header/header-in-view-history.tsx
  22. 22
    263
      web/app/components/workflow/header/index.tsx
  23. 2
    2
      web/app/components/workflow/header/restoring-title.tsx
  24. 2
    2
      web/app/components/workflow/header/view-history.tsx
  25. 2
    0
      web/app/components/workflow/hooks-store/index.ts
  26. 36
    0
      web/app/components/workflow/hooks-store/provider.tsx
  27. 72
    0
      web/app/components/workflow/hooks-store/store.ts
  28. 1
    1
      web/app/components/workflow/hooks/index.ts
  29. 27
    0
      web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts
  30. 0
    17
      web/app/components/workflow/hooks/use-edges-interactions.ts
  31. 12
    0
      web/app/components/workflow/hooks/use-format-time-from-now.ts
  32. 27
    0
      web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts
  33. 0
    17
      web/app/components/workflow/hooks/use-nodes-interactions.ts
  34. 3
    133
      web/app/components/workflow/hooks/use-nodes-sync-draft.ts
  35. 4
    4
      web/app/components/workflow/hooks/use-workflow-interactions.ts
  36. 6
    345
      web/app/components/workflow/hooks/use-workflow-run.ts
  37. 4
    87
      web/app/components/workflow/hooks/use-workflow-start-run.tsx
  38. 4
    126
      web/app/components/workflow/hooks/use-workflow.ts
  39. 46
    145
      web/app/components/workflow/index.tsx
  40. 11
    72
      web/app/components/workflow/panel/index.tsx
  41. 14
    2
      web/app/components/workflow/store/workflow/index.ts
  42. 0
    4
      web/app/components/workflow/store/workflow/node-slice.ts
  43. 4
    6
      web/app/components/workflow/store/workflow/workflow-slice.ts
  44. 6
    2
      web/service/use-workflow.ts

+ 2
- 2
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/workflow/page.tsx Ver arquivo

@@ -1,11 +1,11 @@
'use client'

import Workflow from '@/app/components/workflow'
import WorkflowApp from '@/app/components/workflow-app'

const Page = () => {
return (
<div className='h-full w-full overflow-x-auto'>
<Workflow />
<WorkflowApp />
</div>
)
}

+ 69
- 0
web/app/components/workflow-app/components/workflow-children.tsx Ver arquivo

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

+ 11
- 0
web/app/components/workflow-app/components/workflow-header/chat-variable-trigger.tsx Ver arquivo

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

+ 152
- 0
web/app/components/workflow-app/components/workflow-header/features-trigger.tsx Ver arquivo

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

+ 31
- 0
web/app/components/workflow-app/components/workflow-header/index.tsx Ver arquivo

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

+ 87
- 0
web/app/components/workflow-app/components/workflow-main.tsx Ver arquivo

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

+ 109
- 0
web/app/components/workflow-app/components/workflow-panel.tsx Ver arquivo

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

+ 6
- 0
web/app/components/workflow-app/hooks/index.ts Ver arquivo

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

+ 7
- 0
web/app/components/workflow-app/hooks/use-is-chat-mode.ts Ver arquivo

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

+ 148
- 0
web/app/components/workflow-app/hooks/use-nodes-sync-draft.ts Ver arquivo

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

+ 123
- 0
web/app/components/workflow-app/hooks/use-workflow-init.ts Ver arquivo

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

+ 357
- 0
web/app/components/workflow-app/hooks/use-workflow-run.ts Ver arquivo

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

+ 96
- 0
web/app/components/workflow-app/hooks/use-workflow-start-run.tsx Ver arquivo

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

web/app/components/workflow/hooks/use-workflow-template.ts → web/app/components/workflow-app/hooks/use-workflow-template.ts Ver arquivo

@@ -1,10 +1,10 @@
import { generateNewNode } from '../utils'
import { generateNewNode } from '@/app/components/workflow/utils'
import {
NODE_WIDTH_X_OFFSET,
START_INITIAL_POSITION,
} from '../constants'
import { useIsChatMode } from './use-workflow'
import { useNodesInitialData } from './use-nodes-data'
} from '@/app/components/workflow/constants'
import { useNodesInitialData } from '@/app/components/workflow/hooks'
import { useIsChatMode } from './use-is-chat-mode'

export const useWorkflowTemplate = () => {
const isChatMode = useIsChatMode()

+ 108
- 0
web/app/components/workflow-app/index.tsx Ver arquivo

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

+ 18
- 0
web/app/components/workflow-app/store/workflow/workflow-slice.ts Ver arquivo

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

+ 9
- 4
web/app/components/workflow/context.tsx Ver arquivo

@@ -2,19 +2,24 @@ import {
createContext,
useRef,
} from 'react'
import { createWorkflowStore } from './store'
import {
createWorkflowStore,
} from './store'
import type { StateCreator } from 'zustand'
import type { WorkflowSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice'

type WorkflowStore = ReturnType<typeof createWorkflowStore>
export const WorkflowContext = createContext<WorkflowStore | null>(null)

type WorkflowProviderProps = {
export type WorkflowProviderProps = {
children: React.ReactNode
injectWorkflowStoreSliceFn?: StateCreator<WorkflowSliceShape>
}
export const WorkflowContextProvider = ({ children }: WorkflowProviderProps) => {
export const WorkflowContextProvider = ({ children, injectWorkflowStoreSliceFn }: WorkflowProviderProps) => {
const storeRef = useRef<WorkflowStore | undefined>(undefined)

if (!storeRef.current)
storeRef.current = createWorkflowStore()
storeRef.current = createWorkflowStore({ injectWorkflowStoreSliceFn })

return (
<WorkflowContext.Provider value={storeRef.current}>

+ 2
- 2
web/app/components/workflow/header/editing-title.tsx Ver arquivo

@@ -1,13 +1,13 @@
import { memo } from 'react'
import { useTranslation } from 'react-i18next'
import { useWorkflow } from '../hooks'
import { useFormatTimeFromNow } from '../hooks'
import { useStore } from '@/app/components/workflow/store'
import useTimestamp from '@/hooks/use-timestamp'

const EditingTitle = () => {
const { t } = useTranslation()
const { formatTime } = useTimestamp()
const { formatTimeFromNow } = useWorkflow()
const { formatTimeFromNow } = useFormatTimeFromNow()
const draftUpdatedAt = useStore(state => state.draftUpdatedAt)
const publishedAt = useStore(state => state.publishedAt)
const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft)

+ 69
- 0
web/app/components/workflow/header/header-in-normal.tsx Ver arquivo

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

+ 93
- 0
web/app/components/workflow/header/header-in-restoring.tsx Ver arquivo

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

+ 50
- 0
web/app/components/workflow/header/header-in-view-history.tsx Ver arquivo

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

+ 22
- 263
web/app/components/workflow/header/index.tsx Ver arquivo

@@ -1,292 +1,51 @@
import type { FC } from 'react'
import {
memo,
useCallback,
useMemo,
} from 'react'
import { RiApps2AddLine, RiHistoryLine } from '@remixicon/react'
import { useNodes } from 'reactflow'
import { useTranslation } from 'react-i18next'
import { useContext, useContextSelector } from 'use-context-selector'
import {
useStore,
useWorkflowStore,
} from '../store'
import {
BlockEnum,
InputVarType,
WorkflowVersion,
} from '../types'
import type { StartNodeType } from '../nodes/start/types'
import {
useChecklistBeforePublish,
useIsChatMode,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
useWorkflowMode,
useWorkflowRun,
} from '../hooks'
import AppPublisher from '../../app/app-publisher'
import Toast, { ToastContext } from '../../base/toast'
import Divider from '../../base/divider'
import RunAndHistory from './run-and-history'
import EditingTitle from './editing-title'
import RunningTitle from './running-title'
import RestoringTitle from './restoring-title'
import ViewHistory from './view-history'
import ChatVariableButton from './chat-variable-button'
import EnvButton from './env-button'
import VersionHistoryButton from './version-history-button'
import Button from '@/app/components/base/button'
import { useStore as useAppStore } from '@/app/components/app/store'
import { ArrowNarrowLeft } from '@/app/components/base/icons/src/vender/line/arrows'
import { useFeatures } from '@/app/components/base/features/hooks'
import { usePublishWorkflow, useResetWorkflowVersionHistory } from '@/service/use-workflow'
import type { PublishWorkflowParams } from '@/types/workflow'
import { fetchAppDetail, fetchAppSSO } from '@/service/apps'
import AppContext from '@/context/app-context'

const Header: FC = () => {
const { t } = useTranslation()
const workflowStore = useWorkflowStore()
const appDetail = useAppStore(s => s.appDetail)
const setAppDetail = useAppStore(s => s.setAppDetail)
const systemFeatures = useContextSelector(AppContext, state => state.systemFeatures)
const appID = appDetail?.id
const isChatMode = useIsChatMode()
const { nodesReadOnly, getNodesReadOnly } = useNodesReadOnly()
const { handleNodeSelect } = useNodesInteractions()
const publishedAt = useStore(s => s.publishedAt)
const draftUpdatedAt = useStore(s => s.draftUpdatedAt)
const toolPublished = useStore(s => s.toolPublished)
const currentVersion = useStore(s => s.currentVersion)
const setShowWorkflowVersionHistoryPanel = useStore(s => s.setShowWorkflowVersionHistoryPanel)
const setShowEnvPanel = useStore(s => s.setShowEnvPanel)
const setShowDebugAndPreviewPanel = useStore(s => s.setShowDebugAndPreviewPanel)
const nodes = useNodes<StartNodeType>()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const selectedNode = nodes.find(node => node.data.selected)
const startVariables = startNode?.data.variables
const fileSettings = useFeatures(s => s.features.file)
const variables = useMemo(() => {
const data = startVariables || []
if (fileSettings?.image?.enabled) {
return [
...data,
{
type: InputVarType.files,
variable: '__image',
required: false,
label: 'files',
},
]
}

return data
}, [fileSettings?.image?.enabled, startVariables])

const {
handleLoadBackupDraft,
handleBackupDraft,
} = useWorkflowRun()
const { handleCheckBeforePublish } = useChecklistBeforePublish()
const { handleSyncWorkflowDraft } = useNodesSyncDraft()
const { notify } = useContext(ToastContext)
import type { HeaderInNormalProps } from './header-in-normal'
import HeaderInNormal from './header-in-normal'
import HeaderInHistory from './header-in-view-history'
import type { HeaderInRestoringProps } from './header-in-restoring'
import HeaderInRestoring from './header-in-restoring'

export type HeaderProps = {
normal?: HeaderInNormalProps
restoring?: HeaderInRestoringProps
}
const Header = ({
normal: normalProps,
restoring: restoringProps,
}: HeaderProps) => {
const {
normal,
restoring,
viewHistory,
} = useWorkflowMode()

const handleShowFeatures = useCallback(() => {
const {
showFeaturesPanel,
isRestoring,
setShowFeaturesPanel,
} = workflowStore.getState()
if (getNodesReadOnly() && !isRestoring)
return
setShowFeaturesPanel(!showFeaturesPanel)
}, [workflowStore, getNodesReadOnly])

const handleCancelRestore = useCallback(() => {
handleLoadBackupDraft()
workflowStore.setState({ isRestoring: false })
setShowWorkflowVersionHistoryPanel(false)
}, [workflowStore, handleLoadBackupDraft, setShowWorkflowVersionHistoryPanel])

const resetWorkflowVersionHistory = useResetWorkflowVersionHistory(appDetail!.id)

const handleRestore = useCallback(() => {
setShowWorkflowVersionHistoryPanel(false)
workflowStore.setState({ isRestoring: false })
workflowStore.setState({ backupDraft: undefined })
handleSyncWorkflowDraft(true, false, {
onSuccess: () => {
Toast.notify({
type: 'success',
message: t('workflow.versionHistory.action.restoreSuccess'),
})
},
onError: () => {
Toast.notify({
type: 'error',
message: t('workflow.versionHistory.action.restoreFailure'),
})
},
onSettled: () => {
resetWorkflowVersionHistory()
},
})
}, [handleSyncWorkflowDraft, workflowStore, setShowWorkflowVersionHistoryPanel, resetWorkflowVersionHistory, t])

const updateAppDetail = useCallback(async () => {
try {
const res = await fetchAppDetail({ url: '/apps', id: appID! })
if (systemFeatures.enable_web_sso_switch_component) {
const ssoRes = await fetchAppSSO({ appId: appID! })
setAppDetail({ ...res, enable_sso: ssoRes.enabled })
}
else {
setAppDetail({ ...res })
}
}
catch (error) {
console.error(error)
}
}, [appID, setAppDetail, systemFeatures.enable_web_sso_switch_component])

const { mutateAsync: publishWorkflow } = usePublishWorkflow(appID!)

const onPublish = useCallback(async (params?: PublishWorkflowParams) => {
if (await handleCheckBeforePublish()) {
const res = await publishWorkflow({
title: params?.title || '',
releaseNotes: params?.releaseNotes || '',
})

if (res) {
notify({ type: 'success', message: t('common.api.actionSuccess') })
updateAppDetail()
workflowStore.getState().setPublishedAt(res.created_at)
resetWorkflowVersionHistory()
}
}
else {
throw new Error('Checklist failed')
}
}, [handleCheckBeforePublish, notify, t, workflowStore, publishWorkflow, resetWorkflowVersionHistory, updateAppDetail])

const onStartRestoring = useCallback(() => {
workflowStore.setState({ isRestoring: true })
handleBackupDraft()
// clear right panel
if (selectedNode)
handleNodeSelect(selectedNode.id, true)
setShowWorkflowVersionHistoryPanel(true)
setShowEnvPanel(false)
setShowDebugAndPreviewPanel(false)
}, [handleBackupDraft, workflowStore, handleNodeSelect, selectedNode,
setShowWorkflowVersionHistoryPanel, setShowEnvPanel, setShowDebugAndPreviewPanel])

const onPublisherToggle = useCallback((state: boolean) => {
if (state)
handleSyncWorkflowDraft(true)
}, [handleSyncWorkflowDraft])

const handleGoBackToEdit = useCallback(() => {
handleLoadBackupDraft()
workflowStore.setState({ historyWorkflowData: undefined })
}, [workflowStore, handleLoadBackupDraft])

const handleToolConfigureUpdate = useCallback(() => {
workflowStore.setState({ toolPublished: true })
}, [workflowStore])

return (
<div
className='absolute left-0 top-0 z-10 flex h-14 w-full items-center justify-between bg-mask-top2bottom-gray-50-to-transparent px-3'
>
<div>
{
normal && <EditingTitle />
}
{
viewHistory && <RunningTitle />
}
{
restoring && <RestoringTitle />
}
</div>
{
normal && (
<div className='flex items-center gap-2'>
{/* <GlobalVariableButton disabled={nodesReadOnly} /> */}
{isChatMode && <ChatVariableButton disabled={nodesReadOnly} />}
<EnvButton disabled={nodesReadOnly} />
<Divider type='vertical' className='mx-auto h-3.5' />
<RunAndHistory />
<Button className='text-components-button-secondary-text' onClick={handleShowFeatures}>
<RiApps2AddLine className='mr-1 h-4 w-4 text-components-button-secondary-text' />
{t('workflow.common.features')}
</Button>
<AppPublisher
{...{
publishedAt,
draftUpdatedAt,
disabled: nodesReadOnly,
toolPublished,
inputs: variables,
onRefreshData: handleToolConfigureUpdate,
onPublish,
onToggle: onPublisherToggle,
crossAxisOffset: 4,
}}
/>
<VersionHistoryButton onClick={onStartRestoring} />
</div>
<HeaderInNormal
{...normalProps}
/>
)
}
{
viewHistory && (
<div className='flex items-center space-x-2'>
<ViewHistory withText />
<Divider type='vertical' className='mx-auto h-3.5' />
<Button
variant='primary'
onClick={handleGoBackToEdit}
>
<ArrowNarrowLeft className='mr-1 h-4 w-4' />
{t('workflow.common.goBackToEdit')}
</Button>
</div>
<HeaderInHistory />
)
}
{
restoring && (
<div className='flex items-center justify-end gap-x-2'>
<Button
onClick={handleRestore}
disabled={!currentVersion || currentVersion.version === WorkflowVersion.Draft}
variant='primary'
>
{t('workflow.common.restore')}
</Button>
<Button
className='text-components-button-secondary-accent-text'
onClick={handleCancelRestore}
>
<div className='flex items-center gap-x-0.5'>
<RiHistoryLine className='h-4 w-4' />
<span className='px-0.5'>{t('workflow.common.exitVersions')}</span>
</div>
</Button>
</div>
<HeaderInRestoring
{...restoringProps}
/>
)
}
</div>
)
}

export default memo(Header)
export default Header

+ 2
- 2
web/app/components/workflow/header/restoring-title.tsx Ver arquivo

@@ -1,13 +1,13 @@
import { memo, useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import { useWorkflow } from '../hooks'
import { useFormatTimeFromNow } from '../hooks'
import { useStore } from '../store'
import { WorkflowVersion } from '../types'
import useTimestamp from '@/hooks/use-timestamp'

const RestoringTitle = () => {
const { t } = useTranslation()
const { formatTimeFromNow } = useWorkflow()
const { formatTimeFromNow } = useFormatTimeFromNow()
const { formatTime } = useTimestamp()
const currentVersion = useStore(state => state.currentVersion)
const isDraft = currentVersion?.version === WorkflowVersion.Draft

+ 2
- 2
web/app/components/workflow/header/view-history.tsx Ver arquivo

@@ -11,9 +11,9 @@ import {
RiErrorWarningLine,
} from '@remixicon/react'
import {
useFormatTimeFromNow,
useIsChatMode,
useNodesInteractions,
useWorkflow,
useWorkflowInteractions,
useWorkflowRun,
} from '../hooks'
@@ -50,7 +50,7 @@ const ViewHistory = ({
const { t } = useTranslation()
const isChatMode = useIsChatMode()
const [open, setOpen] = useState(false)
const { formatTimeFromNow } = useWorkflow()
const { formatTimeFromNow } = useFormatTimeFromNow()
const {
handleNodesCancelSelected,
} = useNodesInteractions()

+ 2
- 0
web/app/components/workflow/hooks-store/index.ts Ver arquivo

@@ -0,0 +1,2 @@
export * from './provider'
export * from './store'

+ 36
- 0
web/app/components/workflow/hooks-store/provider.tsx Ver arquivo

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

+ 72
- 0
web/app/components/workflow/hooks-store/store.ts Ver arquivo

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

+ 1
- 1
web/app/components/workflow/hooks/index.ts Ver arquivo

@@ -5,7 +5,6 @@ export * from './use-nodes-data'
export * from './use-nodes-sync-draft'
export * from './use-workflow'
export * from './use-workflow-run'
export * from './use-workflow-template'
export * from './use-checklist'
export * from './use-selection-interactions'
export * from './use-panel-interactions'
@@ -16,3 +15,4 @@ export * from './use-workflow-variables'
export * from './use-shortcuts'
export * from './use-workflow-interactions'
export * from './use-workflow-mode'
export * from './use-format-time-from-now'

+ 27
- 0
web/app/components/workflow/hooks/use-edges-interactions-without-sync.ts Ver arquivo

@@ -0,0 +1,27 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'

export const useEdgesInteractionsWithoutSync = () => {
const store = useStoreApi()

const handleEdgeCancelRunningStatus = useCallback(() => {
const {
edges,
setEdges,
} = store.getState()

const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data._sourceRunningStatus = undefined
edge.data._targetRunningStatus = undefined
edge.data._waitingRun = false
})
})
setEdges(newEdges)
}, [store])

return {
handleEdgeCancelRunningStatus,
}
}

+ 0
- 17
web/app/components/workflow/hooks/use-edges-interactions.ts Ver arquivo

@@ -151,28 +151,11 @@ export const useEdgesInteractions = () => {
setEdges(newEdges)
}, [store, getNodesReadOnly])

const handleEdgeCancelRunningStatus = useCallback(() => {
const {
edges,
setEdges,
} = store.getState()

const newEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
edge.data._sourceRunningStatus = undefined
edge.data._targetRunningStatus = undefined
edge.data._waitingRun = false
})
})
setEdges(newEdges)
}, [store])

return {
handleEdgeEnter,
handleEdgeLeave,
handleEdgeDeleteByDeleteBranch,
handleEdgeDelete,
handleEdgesChange,
handleEdgeCancelRunningStatus,
}
}

+ 12
- 0
web/app/components/workflow/hooks/use-format-time-from-now.ts Ver arquivo

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

+ 27
- 0
web/app/components/workflow/hooks/use-nodes-interactions-without-sync.ts Ver arquivo

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

+ 0
- 17
web/app/components/workflow/hooks/use-nodes-interactions.ts Ver arquivo

@@ -1177,22 +1177,6 @@ export const useNodesInteractions = () => {
saveStateToHistory(WorkflowHistoryEvent.NodeChange)
}, [getNodesReadOnly, store, t, handleSyncWorkflowDraft, saveStateToHistory])

const handleNodeCancelRunningStatus = useCallback(() => {
const {
getNodes,
setNodes,
} = store.getState()

const nodes = getNodes()
const newNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
node.data._runningStatus = undefined
node.data._waitingRun = false
})
})
setNodes(newNodes)
}, [store])

const handleNodesCancelSelected = useCallback(() => {
const {
getNodes,
@@ -1554,7 +1538,6 @@ export const useNodesInteractions = () => {
handleNodeDelete,
handleNodeChange,
handleNodeAdd,
handleNodeCancelRunningStatus,
handleNodesCancelSelected,
handleNodeContextMenu,
handleNodesCopy,

+ 3
- 133
web/app/components/workflow/hooks/use-nodes-sync-draft.ts Ver arquivo

@@ -1,147 +1,17 @@
import { useCallback } from 'react'
import produce from 'immer'
import { useStoreApi } from 'reactflow'
import { useParams } from 'next/navigation'
import {
useStore,
useWorkflowStore,
} from '../store'
import { BlockEnum } from '../types'
import { useWorkflowUpdate } from '../hooks'
import {
useNodesReadOnly,
} from './use-workflow'
import { syncWorkflowDraft } from '@/service/workflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { API_PREFIX } from '@/config'
import { useHooksStore } from '@/app/components/workflow/hooks-store'

export const useNodesSyncDraft = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const { getNodesReadOnly } = useNodesReadOnly()
const { handleRefreshWorkflowDraft } = useWorkflowUpdate()
const debouncedSyncWorkflowDraft = useStore(s => s.debouncedSyncWorkflowDraft)
const params = useParams()

const getPostParams = useCallback(() => {
const {
getNodes,
edges,
transform,
} = store.getState()
const [x, y, zoom] = transform
const {
appId,
conversationVariables,
environmentVariables,
syncWorkflowDraftHash,
} = workflowStore.getState()

if (appId) {
const nodes = getNodes()
const hasStartNode = nodes.find(node => node.data.type === BlockEnum.Start)

if (!hasStartNode)
return

const features = featuresStore!.getState().features
const producedNodes = produce(nodes, (draft) => {
draft.forEach((node) => {
Object.keys(node.data).forEach((key) => {
if (key.startsWith('_'))
delete node.data[key]
})
})
})
const producedEdges = produce(edges, (draft) => {
draft.forEach((edge) => {
Object.keys(edge.data).forEach((key) => {
if (key.startsWith('_'))
delete edge.data[key]
})
})
})
return {
url: `/apps/${appId}/workflows/draft`,
params: {
graph: {
nodes: producedNodes,
edges: producedEdges,
viewport: {
x,
y,
zoom,
},
},
features: {
opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
suggested_questions_after_answer: features.suggested,
text_to_speech: features.text2speech,
speech_to_text: features.speech2text,
retriever_resource: features.citation,
sensitive_word_avoidance: features.moderation,
file_upload: features.file,
},
environment_variables: environmentVariables,
conversation_variables: conversationVariables,
hash: syncWorkflowDraftHash,
},
}
}
}, [store, featuresStore, workflowStore])

const syncWorkflowDraftWhenPageClose = useCallback(() => {
if (getNodesReadOnly())
return
const postParams = getPostParams()

if (postParams) {
navigator.sendBeacon(
`${API_PREFIX}/apps/${params.appId}/workflows/draft?_token=${localStorage.getItem('console_token')}`,
JSON.stringify(postParams.params),
)
}
}, [getPostParams, params.appId, getNodesReadOnly])

const doSyncWorkflowDraft = useCallback(async (
notRefreshWhenSyncError?: boolean,
callback?: {
onSuccess?: () => void
onError?: () => void
onSettled?: () => void
},
) => {
if (getNodesReadOnly())
return
const postParams = getPostParams()

if (postParams) {
const {
setSyncWorkflowDraftHash,
setDraftUpdatedAt,
} = workflowStore.getState()
try {
const res = await syncWorkflowDraft(postParams)
setSyncWorkflowDraftHash(res.hash)
setDraftUpdatedAt(res.updated_at)
callback?.onSuccess && callback.onSuccess()
}
catch (error: any) {
if (error && error.json && !error.bodyUsed) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_sync' && !notRefreshWhenSyncError)
handleRefreshWorkflowDraft()
})
}
callback?.onError && callback.onError()
}
finally {
callback?.onSettled && callback.onSettled()
}
}
}, [workflowStore, getPostParams, getNodesReadOnly, handleRefreshWorkflowDraft])
const doSyncWorkflowDraft = useHooksStore(s => s.doSyncWorkflowDraft)
const syncWorkflowDraftWhenPageClose = useHooksStore(s => s.syncWorkflowDraftWhenPageClose)

const handleSyncWorkflowDraft = useCallback((
sync?: boolean,

+ 4
- 4
web/app/components/workflow/hooks/use-workflow-interactions.ts Ver arquivo

@@ -25,8 +25,8 @@ import {
useSelectionInteractions,
useWorkflowReadOnly,
} from '../hooks'
import { useEdgesInteractions } from './use-edges-interactions'
import { useNodesInteractions } from './use-nodes-interactions'
import { useEdgesInteractionsWithoutSync } from './use-edges-interactions-without-sync'
import { useNodesInteractionsWithoutSync } from './use-nodes-interactions-without-sync'
import { useNodesSyncDraft } from './use-nodes-sync-draft'
import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history'
import { useEventEmitterContextContext } from '@/context/event-emitter'
@@ -37,8 +37,8 @@ import { useStore as useAppStore } from '@/app/components/app/store'

export const useWorkflowInteractions = () => {
const workflowStore = useWorkflowStore()
const { handleNodeCancelRunningStatus } = useNodesInteractions()
const { handleEdgeCancelRunningStatus } = useEdgesInteractions()
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()

const handleCancelDebugAndPreviewPanel = useCallback(() => {
workflowStore.setState({

+ 6
- 345
web/app/components/workflow/hooks/use-workflow-run.ts Ver arquivo

@@ -1,350 +1,11 @@
import { useCallback } from 'react'
import {
useReactFlow,
useStoreApi,
} from 'reactflow'
import produce from 'immer'
import { v4 as uuidV4 } from 'uuid'
import { usePathname } from 'next/navigation'
import { useWorkflowStore } from '../store'
import { useNodesSyncDraft } from '../hooks'
import { WorkflowRunningStatus } from '../types'
import { useWorkflowUpdate } from './use-workflow-interactions'
import { useWorkflowRunEvent } from './use-workflow-run-event/use-workflow-run-event'
import { useStore as useAppStore } from '@/app/components/app/store'
import type { IOtherOptions } from '@/service/base'
import { ssePost } from '@/service/base'
import { stopWorkflowRun } from '@/service/workflow'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
import type { VersionHistory } from '@/types/workflow'
import { noop } from 'lodash-es'
import { useHooksStore } from '@/app/components/workflow/hooks-store'

export const useWorkflowRun = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const featuresStore = useFeaturesStore()
const { doSyncWorkflowDraft } = useNodesSyncDraft()
const { handleUpdateWorkflowCanvas } = useWorkflowUpdate()
const pathname = usePathname()
const {
handleWorkflowStarted,
handleWorkflowFinished,
handleWorkflowFailed,
handleWorkflowNodeStarted,
handleWorkflowNodeFinished,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
handleWorkflowNodeLoopStarted,
handleWorkflowNodeLoopNext,
handleWorkflowNodeLoopFinished,
handleWorkflowNodeRetry,
handleWorkflowAgentLog,
handleWorkflowTextChunk,
handleWorkflowTextReplace,
} = useWorkflowRunEvent()

const handleBackupDraft = useCallback(() => {
const {
getNodes,
edges,
} = store.getState()
const { getViewport } = reactflow
const {
backupDraft,
setBackupDraft,
environmentVariables,
} = workflowStore.getState()
const { features } = featuresStore!.getState()

if (!backupDraft) {
setBackupDraft({
nodes: getNodes(),
edges,
viewport: getViewport(),
features,
environmentVariables,
})
doSyncWorkflowDraft()
}
}, [reactflow, workflowStore, store, featuresStore, doSyncWorkflowDraft])

const handleLoadBackupDraft = useCallback(() => {
const {
backupDraft,
setBackupDraft,
setEnvironmentVariables,
} = workflowStore.getState()

if (backupDraft) {
const {
nodes,
edges,
viewport,
features,
environmentVariables,
} = backupDraft
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
setEnvironmentVariables(environmentVariables)
featuresStore!.setState({ features })
setBackupDraft(undefined)
}
}, [handleUpdateWorkflowCanvas, workflowStore, featuresStore])

const handleRun = useCallback(async (
params: any,
callback?: IOtherOptions,
) => {
const {
getNodes,
setNodes,
} = store.getState()
const newNodes = produce(getNodes(), (draft) => {
draft.forEach((node) => {
node.data.selected = false
node.data._runningStatus = undefined
})
})
setNodes(newNodes)
await doSyncWorkflowDraft()

const {
onWorkflowStarted,
onWorkflowFinished,
onNodeStarted,
onNodeFinished,
onIterationStart,
onIterationNext,
onIterationFinish,
onLoopStart,
onLoopNext,
onLoopFinish,
onNodeRetry,
onAgentLog,
onError,
...restCallback
} = callback || {}
workflowStore.setState({ historyWorkflowData: undefined })
const appDetail = useAppStore.getState().appDetail
const workflowContainer = document.getElementById('workflow-container')

const {
clientWidth,
clientHeight,
} = workflowContainer!

let url = ''
if (appDetail?.mode === 'advanced-chat')
url = `/apps/${appDetail.id}/advanced-chat/workflows/draft/run`

if (appDetail?.mode === 'workflow')
url = `/apps/${appDetail.id}/workflows/draft/run`

const {
setWorkflowRunningData,
} = workflowStore.getState()
setWorkflowRunningData({
result: {
status: WorkflowRunningStatus.Running,
},
tracing: [],
resultText: '',
})

let ttsUrl = ''
let ttsIsPublic = false
if (params.token) {
ttsUrl = '/text-to-audio'
ttsIsPublic = true
}
else if (params.appId) {
if (pathname.search('explore/installed') > -1)
ttsUrl = `/installed-apps/${params.appId}/text-to-audio`
else
ttsUrl = `/apps/${params.appId}/text-to-audio`
}
const player = AudioPlayerManager.getInstance().getAudioPlayer(ttsUrl, ttsIsPublic, uuidV4(), 'none', 'none', noop)

ssePost(
url,
{
body: params,
},
{
onWorkflowStarted: (params) => {
handleWorkflowStarted(params)

if (onWorkflowStarted)
onWorkflowStarted(params)
},
onWorkflowFinished: (params) => {
handleWorkflowFinished(params)

if (onWorkflowFinished)
onWorkflowFinished(params)
},
onError: (params) => {
handleWorkflowFailed()

if (onError)
onError(params)
},
onNodeStarted: (params) => {
handleWorkflowNodeStarted(
params,
{
clientWidth,
clientHeight,
},
)

if (onNodeStarted)
onNodeStarted(params)
},
onNodeFinished: (params) => {
handleWorkflowNodeFinished(params)

if (onNodeFinished)
onNodeFinished(params)
},
onIterationStart: (params) => {
handleWorkflowNodeIterationStarted(
params,
{
clientWidth,
clientHeight,
},
)

if (onIterationStart)
onIterationStart(params)
},
onIterationNext: (params) => {
handleWorkflowNodeIterationNext(params)

if (onIterationNext)
onIterationNext(params)
},
onIterationFinish: (params) => {
handleWorkflowNodeIterationFinished(params)

if (onIterationFinish)
onIterationFinish(params)
},
onLoopStart: (params) => {
handleWorkflowNodeLoopStarted(
params,
{
clientWidth,
clientHeight,
},
)

if (onLoopStart)
onLoopStart(params)
},
onLoopNext: (params) => {
handleWorkflowNodeLoopNext(params)

if (onLoopNext)
onLoopNext(params)
},
onLoopFinish: (params) => {
handleWorkflowNodeLoopFinished(params)

if (onLoopFinish)
onLoopFinish(params)
},
onNodeRetry: (params) => {
handleWorkflowNodeRetry(params)

if (onNodeRetry)
onNodeRetry(params)
},
onAgentLog: (params) => {
handleWorkflowAgentLog(params)

if (onAgentLog)
onAgentLog(params)
},
onTextChunk: (params) => {
handleWorkflowTextChunk(params)
},
onTextReplace: (params) => {
handleWorkflowTextReplace(params)
},
onTTSChunk: (messageId: string, audio: string) => {
if (!audio || audio === '')
return
player.playAudioWithAudio(audio, true)
AudioPlayerManager.getInstance().resetMsgId(messageId)
},
onTTSEnd: (messageId: string, audio: string) => {
player.playAudioWithAudio(audio, false)
},
...restCallback,
},
)
}, [
store,
workflowStore,
doSyncWorkflowDraft,
handleWorkflowStarted,
handleWorkflowFinished,
handleWorkflowFailed,
handleWorkflowNodeStarted,
handleWorkflowNodeFinished,
handleWorkflowNodeIterationStarted,
handleWorkflowNodeIterationNext,
handleWorkflowNodeIterationFinished,
handleWorkflowNodeLoopStarted,
handleWorkflowNodeLoopNext,
handleWorkflowNodeLoopFinished,
handleWorkflowNodeRetry,
handleWorkflowTextChunk,
handleWorkflowTextReplace,
handleWorkflowAgentLog,
pathname],
)

const handleStopRun = useCallback((taskId: string) => {
const appId = useAppStore.getState().appDetail?.id

stopWorkflowRun(`/apps/${appId}/workflow-runs/tasks/${taskId}/stop`)
}, [])

const handleRestoreFromPublishedWorkflow = useCallback((publishedWorkflow: VersionHistory) => {
const nodes = publishedWorkflow.graph.nodes.map(node => ({ ...node, selected: false, data: { ...node.data, selected: false } }))
const edges = publishedWorkflow.graph.edges
const viewport = publishedWorkflow.graph.viewport!
handleUpdateWorkflowCanvas({
nodes,
edges,
viewport,
})
const mappedFeatures = {
opening: {
enabled: !!publishedWorkflow.features.opening_statement || !!publishedWorkflow.features.suggested_questions.length,
opening_statement: publishedWorkflow.features.opening_statement,
suggested_questions: publishedWorkflow.features.suggested_questions,
},
suggested: publishedWorkflow.features.suggested_questions_after_answer,
text2speech: publishedWorkflow.features.text_to_speech,
speech2text: publishedWorkflow.features.speech_to_text,
citation: publishedWorkflow.features.retriever_resource,
moderation: publishedWorkflow.features.sensitive_word_avoidance,
file: publishedWorkflow.features.file_upload,
}

featuresStore?.setState({ features: mappedFeatures })
workflowStore.getState().setEnvironmentVariables(publishedWorkflow.environment_variables || [])
}, [featuresStore, handleUpdateWorkflowCanvas, workflowStore])
const handleBackupDraft = useHooksStore(s => s.handleBackupDraft)
const handleLoadBackupDraft = useHooksStore(s => s.handleLoadBackupDraft)
const handleRestoreFromPublishedWorkflow = useHooksStore(s => s.handleRestoreFromPublishedWorkflow)
const handleRun = useHooksStore(s => s.handleRun)
const handleStopRun = useHooksStore(s => s.handleStopRun)

return {
handleBackupDraft,

+ 4
- 87
web/app/components/workflow/hooks/use-workflow-start-run.tsx Ver arquivo

@@ -1,92 +1,9 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { useWorkflowStore } from '../store'
import {
BlockEnum,
WorkflowRunningStatus,
} from '../types'
import {
useIsChatMode,
useNodesSyncDraft,
useWorkflowInteractions,
useWorkflowRun,
} from './index'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { useHooksStore } from '@/app/components/workflow/hooks-store'

export const useWorkflowStartRun = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const featuresStore = useFeaturesStore()
const isChatMode = useIsChatMode()
const { handleCancelDebugAndPreviewPanel } = useWorkflowInteractions()
const { handleRun } = useWorkflowRun()
const { doSyncWorkflowDraft } = useNodesSyncDraft()

const handleWorkflowStartRunInWorkflow = useCallback(async () => {
const {
workflowRunningData,
} = workflowStore.getState()

if (workflowRunningData?.result.status === WorkflowRunningStatus.Running)
return

const { getNodes } = store.getState()
const nodes = getNodes()
const startNode = nodes.find(node => node.data.type === BlockEnum.Start)
const startVariables = startNode?.data.variables || []
const fileSettings = featuresStore!.getState().features.file
const {
showDebugAndPreviewPanel,
setShowDebugAndPreviewPanel,
setShowInputsPanel,
setShowEnvPanel,
} = workflowStore.getState()

setShowEnvPanel(false)

if (showDebugAndPreviewPanel) {
handleCancelDebugAndPreviewPanel()
return
}

if (!startVariables.length && !fileSettings?.image?.enabled) {
await doSyncWorkflowDraft()
handleRun({ inputs: {}, files: [] })
setShowDebugAndPreviewPanel(true)
setShowInputsPanel(false)
}
else {
setShowDebugAndPreviewPanel(true)
setShowInputsPanel(true)
}
}, [store, workflowStore, featuresStore, handleCancelDebugAndPreviewPanel, handleRun, doSyncWorkflowDraft])

const handleWorkflowStartRunInChatflow = useCallback(async () => {
const {
showDebugAndPreviewPanel,
setShowDebugAndPreviewPanel,
setHistoryWorkflowData,
setShowEnvPanel,
setShowChatVariablePanel,
} = workflowStore.getState()

setShowEnvPanel(false)
setShowChatVariablePanel(false)

if (showDebugAndPreviewPanel)
handleCancelDebugAndPreviewPanel()
else
setShowDebugAndPreviewPanel(true)

setHistoryWorkflowData(undefined)
}, [workflowStore, handleCancelDebugAndPreviewPanel])

const handleStartWorkflowRun = useCallback(() => {
if (!isChatMode)
handleWorkflowStartRunInWorkflow()
else
handleWorkflowStartRunInChatflow()
}, [isChatMode, handleWorkflowStartRunInWorkflow, handleWorkflowStartRunInChatflow])
const handleStartWorkflowRun = useHooksStore(s => s.handleStartWorkflowRun)
const handleWorkflowStartRunInWorkflow = useHooksStore(s => s.handleWorkflowStartRunInWorkflow)
const handleWorkflowStartRunInChatflow = useHooksStore(s => s.handleWorkflowStartRunInChatflow)

return {
handleStartWorkflowRun,

+ 4
- 126
web/app/components/workflow/hooks/use-workflow.ts Ver arquivo

@@ -1,13 +1,9 @@
import {
useCallback,
useEffect,
useMemo,
useState,
} from 'react'
import dayjs from 'dayjs'
import { uniqBy } from 'lodash-es'
import { useTranslation } from 'react-i18next'
import { useContext } from 'use-context-selector'
import {
getIncomers,
getOutgoers,
@@ -40,25 +36,15 @@ import {
import { CUSTOM_NOTE_NODE } from '../note-node/constants'
import { findUsedVarNodes, getNodeOutputVars, updateNodeVars } from '../nodes/_base/components/variable/utils'
import { useNodesExtraData } from './use-nodes-data'
import { useWorkflowTemplate } from './use-workflow-template'
import { useStore as useAppStore } from '@/app/components/app/store'
import {
fetchNodesDefaultConfigs,
fetchPublishedWorkflow,
fetchWorkflowDraft,
syncWorkflowDraft,
} from '@/service/workflow'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
import {
fetchAllBuiltInTools,
fetchAllCustomTools,
fetchAllWorkflowTools,
} from '@/service/tools'
import I18n from '@/context/i18n'
import { CollectionType } from '@/app/components/tools/types'
import { CUSTOM_ITERATION_START_NODE } from '@/app/components/workflow/nodes/iteration-start/constants'
import { CUSTOM_LOOP_START_NODE } from '@/app/components/workflow/nodes/loop-start/constants'
import { useWorkflowConfig } from '@/service/use-workflow'
import { basePath } from '@/utils/var'
import { canFindTool } from '@/utils'

@@ -70,12 +56,9 @@ export const useIsChatMode = () => {

export const useWorkflow = () => {
const { t } = useTranslation()
const { locale } = useContext(I18n)
const store = useStoreApi()
const workflowStore = useWorkflowStore()
const appId = useStore(s => s.appId)
const nodesExtraData = useNodesExtraData()
const { data: workflowConfig } = useWorkflowConfig(appId)
const setPanelWidth = useCallback((width: number) => {
localStorage.setItem('workflow-node-panel-width', `${width}`)
workflowStore.setState({ panelWidth: width })
@@ -120,7 +103,7 @@ export const useWorkflow = () => {

list.push(...incomers)

return uniqBy(list, 'id').filter((item) => {
return uniqBy(list, 'id').filter((item: Node) => {
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
})
}, [store])
@@ -167,7 +150,7 @@ export const useWorkflow = () => {

const length = list.length
if (length) {
return uniqBy(list, 'id').reverse().filter((item) => {
return uniqBy(list, 'id').reverse().filter((item: Node) => {
return SUPPORT_OUTPUT_VARS_NODE.includes(item.data.type)
})
}
@@ -344,6 +327,7 @@ export const useWorkflow = () => {
parallelList,
hasAbnormalEdges,
} = getParallelInfo(nodes, edges, parentNodeId)
const { workflowConfig } = workflowStore.getState()

if (hasAbnormalEdges)
return false
@@ -359,7 +343,7 @@ export const useWorkflow = () => {
}

return true
}, [t, workflowStore, workflowConfig?.parallel_depth_limit])
}, [t, workflowStore])

const isValidConnection = useCallback(({ source, sourceHandle, target }: Connection) => {
const {
@@ -407,10 +391,6 @@ export const useWorkflow = () => {
return !hasCycle(targetNode)
}, [store, nodesExtraData, checkParallelLimit])

const formatTimeFromNow = useCallback((time: number) => {
return dayjs(time).locale(locale === 'zh-Hans' ? 'zh-cn' : locale).fromNow()
}, [locale])

const getNode = useCallback((nodeId?: string) => {
const { getNodes } = store.getState()
const nodes = getNodes()
@@ -432,7 +412,6 @@ export const useWorkflow = () => {
checkNestedParallelLimit,
isValidConnection,
isFromStartNode,
formatTimeFromNow,
getNode,
getBeforeNodeById,
getIterationNodeChildren,
@@ -478,107 +457,6 @@ export const useFetchToolsData = () => {
}
}

export const useWorkflowInit = () => {
const workflowStore = useWorkflowStore()
const {
nodes: nodesTemplate,
edges: edgesTemplate,
} = useWorkflowTemplate()
const { handleFetchAllTools } = useFetchToolsData()
const appDetail = useAppStore(state => state.appDetail)!
const setSyncWorkflowDraftHash = useStore(s => s.setSyncWorkflowDraftHash)
const [data, setData] = useState<FetchWorkflowDraftResponse>()
const [isLoading, setIsLoading] = useState(true)
useEffect(() => {
workflowStore.setState({ appId: appDetail.id })
}, [appDetail.id, workflowStore])

const handleGetInitialWorkflowData = useCallback(async () => {
try {
const res = await fetchWorkflowDraft(`/apps/${appDetail.id}/workflows/draft`)
setData(res)
workflowStore.setState({
envSecrets: (res.environment_variables || []).filter(env => env.value_type === 'secret').reduce((acc, env) => {
acc[env.id] = env.value
return acc
}, {} as Record<string, string>),
environmentVariables: res.environment_variables?.map(env => env.value_type === 'secret' ? { ...env, value: '[__HIDDEN__]' } : env) || [],
conversationVariables: res.conversation_variables || [],
})
setSyncWorkflowDraftHash(res.hash)
setIsLoading(false)
}
catch (error: any) {
if (error && error.json && !error.bodyUsed && appDetail) {
error.json().then((err: any) => {
if (err.code === 'draft_workflow_not_exist') {
workflowStore.setState({ notInitialWorkflow: true })
syncWorkflowDraft({
url: `/apps/${appDetail.id}/workflows/draft`,
params: {
graph: {
nodes: nodesTemplate,
edges: edgesTemplate,
},
features: {
retriever_resource: { enabled: true },
},
environment_variables: [],
conversation_variables: [],
},
}).then((res) => {
workflowStore.getState().setDraftUpdatedAt(res.updated_at)
handleGetInitialWorkflowData()
})
}
})
}
}
}, [appDetail, nodesTemplate, edgesTemplate, workflowStore, setSyncWorkflowDraftHash])

useEffect(() => {
handleGetInitialWorkflowData()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [])

const handleFetchPreloadData = useCallback(async () => {
try {
const nodesDefaultConfigsData = await fetchNodesDefaultConfigs(`/apps/${appDetail?.id}/workflows/default-workflow-block-configs`)
const publishedWorkflow = await fetchPublishedWorkflow(`/apps/${appDetail?.id}/workflows/publish`)
workflowStore.setState({
nodesDefaultConfigs: nodesDefaultConfigsData.reduce((acc, block) => {
if (!acc[block.type])
acc[block.type] = { ...block.config }
return acc
}, {} as Record<string, any>),
})
workflowStore.getState().setPublishedAt(publishedWorkflow?.created_at)
}
catch (e) {
console.error(e)
}
}, [workflowStore, appDetail])

useEffect(() => {
handleFetchPreloadData()
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
}, [handleFetchPreloadData, handleFetchAllTools])

useEffect(() => {
if (data) {
workflowStore.getState().setDraftUpdatedAt(data.updated_at)
workflowStore.getState().setToolPublished(data.tool_published)
}
}, [data, workflowStore])

return {
data,
isLoading,
}
}

export const useWorkflowReadOnly = () => {
const workflowStore = useWorkflowStore()
const workflowRunningData = useStore(s => s.workflowRunningData)

+ 46
- 145
web/app/components/workflow/index.tsx Ver arquivo

@@ -5,11 +5,8 @@ import {
memo,
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from 'react'
import useSWR from 'swr'
import { setAutoFreeze } from 'immer'
import {
useEventListener,
@@ -31,17 +28,14 @@ import 'reactflow/dist/style.css'
import './style.css'
import type {
Edge,
EnvironmentVariable,
Node,
} from './types'
import {
ControlMode,
SupportUploadFileTypes,
} from './types'
import { WorkflowContextProvider } from './context'
import {
useDSL,
useEdgesInteractions,
useFetchToolsData,
useNodesInteractions,
useNodesReadOnly,
useNodesSyncDraft,
@@ -49,11 +43,9 @@ import {
useSelectionInteractions,
useShortcuts,
useWorkflow,
useWorkflowInit,
useWorkflowReadOnly,
useWorkflowUpdate,
} from './hooks'
import Header from './header'
import CustomNode from './nodes'
import CustomNoteNode from './note-node'
import { CUSTOM_NOTE_NODE } from './note-node/constants'
@@ -66,42 +58,28 @@ import { CUSTOM_SIMPLE_NODE } from './simple-node/constants'
import Operator from './operator'
import CustomEdge from './custom-edge'
import CustomConnectionLine from './custom-connection-line'
import Panel from './panel'
import Features from './features'
import HelpLine from './help-line'
import CandidateNode from './candidate-node'
import PanelContextmenu from './panel-contextmenu'
import NodeContextmenu from './node-contextmenu'
import SyncingDataModal from './syncing-data-modal'
import UpdateDSLModal from './update-dsl-modal'
import DSLExportConfirmModal from './dsl-export-confirm-modal'
import LimitTips from './limit-tips'
import PluginDependency from './plugin-dependency'
import {
useStore,
useWorkflowStore,
} from './store'
import {
initialEdges,
initialNodes,
} from './utils'
import {
CUSTOM_EDGE,
CUSTOM_NODE,
DSL_EXPORT_CHECK,
ITERATION_CHILDREN_Z_INDEX,
WORKFLOW_DATA_UPDATE,
} from './constants'
import { WorkflowHistoryProvider } from './workflow-history-store'
import Loading from '@/app/components/base/loading'
import { FeaturesProvider } from '@/app/components/base/features'
import type { Features as FeaturesData } from '@/app/components/base/features/types'
import { useFeaturesStore } from '@/app/components/base/features/hooks'
import { useEventEmitterContextContext } from '@/context/event-emitter'
import Confirm from '@/app/components/base/confirm'
import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
import { fetchFileUploadConfig } from '@/service/common'
import DatasetsDetailProvider from './datasets-detail-store/provider'
import { HooksStoreContextProvider } from './hooks-store'
import type { Shape as HooksStoreShape } from './hooks-store'

const nodeTypes = {
[CUSTOM_NODE]: CustomNode,
@@ -114,32 +92,32 @@ const edgeTypes = {
[CUSTOM_EDGE]: CustomEdge,
}

type WorkflowProps = {
export type WorkflowProps = {
nodes: Node[]
edges: Edge[]
viewport?: Viewport
children?: React.ReactNode
onWorkflowDataUpdate?: (v: any) => void
}
const Workflow: FC<WorkflowProps> = memo(({
export const Workflow: FC<WorkflowProps> = memo(({
nodes: originalNodes,
edges: originalEdges,
viewport,
children,
onWorkflowDataUpdate,
}) => {
const workflowContainerRef = useRef<HTMLDivElement>(null)
const workflowStore = useWorkflowStore()
const reactflow = useReactFlow()
const featuresStore = useFeaturesStore()
const [nodes, setNodes] = useNodesState(originalNodes)
const [edges, setEdges] = useEdgesState(originalEdges)
const showFeaturesPanel = useStore(state => state.showFeaturesPanel)
const controlMode = useStore(s => s.controlMode)
const nodeAnimation = useStore(s => s.nodeAnimation)
const showConfirm = useStore(s => s.showConfirm)
const showImportDSLModal = useStore(s => s.showImportDSLModal)

const {
setShowConfirm,
setControlPromptEditorRerenderKey,
setShowImportDSLModal,
setSyncWorkflowDraftHash,
} = workflowStore.getState()
const {
@@ -148,9 +126,6 @@ const Workflow: FC<WorkflowProps> = memo(({
} = useNodesSyncDraft()
const { workflowReadOnly } = useWorkflowReadOnly()
const { nodesReadOnly } = useNodesReadOnly()

const [secretEnvList, setSecretEnvList] = useState<EnvironmentVariable[]>([])

const { eventEmitter } = useEventEmitterContextContext()

eventEmitter?.useSubscription((v: any) => {
@@ -161,19 +136,13 @@ const Workflow: FC<WorkflowProps> = memo(({
if (v.payload.viewport)
reactflow.setViewport(v.payload.viewport)

if (v.payload.features && featuresStore) {
const { setFeatures } = featuresStore.getState()

setFeatures(v.payload.features)
}

if (v.payload.hash)
setSyncWorkflowDraftHash(v.payload.hash)

onWorkflowDataUpdate?.(v.payload)

setTimeout(() => setControlPromptEditorRerenderKey(Date.now()))
}
if (v.type === DSL_EXPORT_CHECK)
setSecretEnvList(v.payload.data as EnvironmentVariable[])
})

useEffect(() => {
@@ -231,6 +200,12 @@ const Workflow: FC<WorkflowProps> = memo(({
})
}
})
const { handleFetchAllTools } = useFetchToolsData()
useEffect(() => {
handleFetchAllTools('builtin')
handleFetchAllTools('custom')
handleFetchAllTools('workflow')
}, [handleFetchAllTools])

const {
handleNodeDragStart,
@@ -258,15 +233,10 @@ const Workflow: FC<WorkflowProps> = memo(({
} = useSelectionInteractions()
const {
handlePaneContextMenu,
handlePaneContextmenuCancel,
} = usePanelInteractions()
const {
isValidConnection,
} = useWorkflow()
const {
exportCheck,
handleExportDSL,
} = useDSL()

useOnViewportChange({
onEnd: () => {
@@ -297,12 +267,7 @@ const Workflow: FC<WorkflowProps> = memo(({
>
<SyncingDataModal />
<CandidateNode />
<Header />
<Panel />
<Operator handleRedo={handleHistoryForward} handleUndo={handleHistoryBack} />
{
showFeaturesPanel && <Features />
}
<PanelContextmenu />
<NodeContextmenu />
<HelpLine />
@@ -317,26 +282,8 @@ const Workflow: FC<WorkflowProps> = memo(({
/>
)
}
{
showImportDSLModal && (
<UpdateDSLModal
onCancel={() => setShowImportDSLModal(false)}
onBackup={exportCheck}
onImport={handlePaneContextmenuCancel}
/>
)
}
{
secretEnvList.length > 0 && (
<DSLExportConfirmModal
envList={secretEnvList}
onConfirm={handleExportDSL}
onClose={() => setSecretEnvList([])}
/>
)
}
<LimitTips />
<PluginDependency />
{children}
<ReactFlow
nodeTypes={nodeTypes}
edgeTypes={edgeTypes}
@@ -389,89 +336,43 @@ const Workflow: FC<WorkflowProps> = memo(({
</div>
)
})
Workflow.displayName = 'Workflow'

const WorkflowWrap = memo(() => {
const {
data,
isLoading,
} = useWorkflowInit()
const { data: fileUploadConfigResponse } = useSWR({ url: '/files/upload' }, fetchFileUploadConfig)

const nodesData = useMemo(() => {
if (data)
return initialNodes(data.graph.nodes, data.graph.edges)

return []
}, [data])
const edgesData = useMemo(() => {
if (data)
return initialEdges(data.graph.edges, data.graph.nodes)

return []
}, [data])

if (!data || isLoading) {
return (
<div className='relative flex h-full w-full items-center justify-center'>
<Loading />
</div>
)
}
type WorkflowWithInnerContextProps = WorkflowProps & {
hooksStore?: Partial<HooksStoreShape>
}
export const WorkflowWithInnerContext = memo(({
hooksStore,
...restProps
}: WorkflowWithInnerContextProps) => {
return (
<HooksStoreContextProvider {...hooksStore}>
<Workflow {...restProps} />
</HooksStoreContextProvider>
)
})

const features = data.features || {}
const initialFeatures: FeaturesData = {
file: {
image: {
enabled: !!features.file_upload?.image?.enabled,
number_limits: features.file_upload?.image?.number_limits || 3,
transfer_methods: features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
},
enabled: !!(features.file_upload?.enabled || features.file_upload?.image?.enabled),
allowed_file_types: features.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
allowed_file_extensions: features.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
allowed_file_upload_methods: features.file_upload?.allowed_file_upload_methods || features.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
number_limits: features.file_upload?.number_limits || features.file_upload?.image?.number_limits || 3,
fileUploadConfig: fileUploadConfigResponse,
},
opening: {
enabled: !!features.opening_statement,
opening_statement: features.opening_statement,
suggested_questions: features.suggested_questions,
},
suggested: features.suggested_questions_after_answer || { enabled: false },
speech2text: features.speech_to_text || { enabled: false },
text2speech: features.text_to_speech || { enabled: false },
citation: features.retriever_resource || { enabled: false },
moderation: features.sensitive_word_avoidance || { enabled: false },
type WorkflowWithDefaultContextProps =
Pick<WorkflowProps, 'edges' | 'nodes'>
& {
children: React.ReactNode
}

const WorkflowWithDefaultContext = ({
nodes,
edges,
children,
}: WorkflowWithDefaultContextProps) => {
return (
<ReactFlowProvider>
<WorkflowHistoryProvider
nodes={nodesData}
edges={edgesData} >
<FeaturesProvider features={initialFeatures}>
<DatasetsDetailProvider nodes={nodesData}>
<Workflow
nodes={nodesData}
edges={edgesData}
viewport={data?.graph.viewport}
/>
</DatasetsDetailProvider>
</FeaturesProvider>
nodes={nodes}
edges={edges} >
<DatasetsDetailProvider nodes={nodes}>
{children}
</DatasetsDetailProvider>
</WorkflowHistoryProvider>
</ReactFlowProvider>
)
})
WorkflowWrap.displayName = 'WorkflowWrap'

const WorkflowContainer = () => {
return (
<WorkflowContextProvider>
<WorkflowWrap />
</WorkflowContextProvider>
)
}

export default memo(WorkflowContainer)
export default memo(WorkflowWithDefaultContext)

+ 11
- 72
web/app/components/workflow/panel/index.tsx Ver arquivo

@@ -1,43 +1,25 @@
import type { FC } from 'react'
import { memo } from 'react'
import { useNodes } from 'reactflow'
import { useShallow } from 'zustand/react/shallow'
import type { CommonNodeType } from '../types'
import { Panel as NodePanel } from '../nodes'
import { useStore } from '../store'
import {
useIsChatMode,
} from '../hooks'
import DebugAndPreview from './debug-and-preview'
import Record from './record'
import WorkflowPreview from './workflow-preview'
import ChatRecord from './chat-record'
import ChatVariablePanel from './chat-variable-panel'
import EnvPanel from './env-panel'
import GlobalVariablePanel from './global-variable-panel'
import VersionHistoryPanel from './version-history-panel'
import cn from '@/utils/classnames'
import { useStore as useAppStore } from '@/app/components/app/store'
import MessageLogModal from '@/app/components/base/message-log-modal'

const Panel: FC = () => {
export type PanelProps = {
components?: {
left?: React.ReactNode
right?: React.ReactNode
}
}
const Panel: FC<PanelProps> = ({
components,
}) => {
const nodes = useNodes<CommonNodeType>()
const isChatMode = useIsChatMode()
const selectedNode = nodes.find(node => node.data.selected)
const historyWorkflowData = useStore(s => s.historyWorkflowData)
const showDebugAndPreviewPanel = useStore(s => s.showDebugAndPreviewPanel)
const showEnvPanel = useStore(s => s.showEnvPanel)
const showChatVariablePanel = useStore(s => s.showChatVariablePanel)
const showGlobalVariablePanel = useStore(s => s.showGlobalVariablePanel)
const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel)
const isRestoring = useStore(s => s.isRestoring)
const { currentLogItem, setCurrentLogItem, showMessageLogModal, setShowMessageLogModal, currentLogModalActiveTab } = useAppStore(useShallow(state => ({
currentLogItem: state.currentLogItem,
setCurrentLogItem: state.setCurrentLogItem,
showMessageLogModal: state.showMessageLogModal,
setShowMessageLogModal: state.setShowMessageLogModal,
currentLogModalActiveTab: state.currentLogModalActiveTab,
})))

return (
<div
@@ -46,18 +28,7 @@ const Panel: FC = () => {
key={`${isRestoring}`}
>
{
showMessageLogModal && (
<MessageLogModal
fixedWidth
width={400}
currentLogItem={currentLogItem}
onCancel={() => {
setCurrentLogItem()
setShowMessageLogModal(false)
}}
defaultTab={currentLogModalActiveTab}
/>
)
components?.left
}
{
!!selectedNode && (
@@ -65,45 +36,13 @@ const Panel: FC = () => {
)
}
{
historyWorkflowData && !isChatMode && (
<Record />
)
}
{
historyWorkflowData && isChatMode && (
<ChatRecord />
)
}
{
showDebugAndPreviewPanel && isChatMode && (
<DebugAndPreview />
)
}
{
showDebugAndPreviewPanel && !isChatMode && (
<WorkflowPreview />
)
components?.right
}
{
showEnvPanel && (
<EnvPanel />
)
}
{
showChatVariablePanel && (
<ChatVariablePanel />
)
}
{
showGlobalVariablePanel && (
<GlobalVariablePanel />
)
}
{
showWorkflowVersionHistoryPanel && (
<VersionHistoryPanel/>
)
}
</div>
)
}

+ 14
- 2
web/app/components/workflow/store/workflow/index.ts Ver arquivo

@@ -1,4 +1,7 @@
import { useContext } from 'react'
import type {
StateCreator,
} from 'zustand'
import {
useStore as useZustandStore,
} from 'zustand'
@@ -26,6 +29,7 @@ import { createWorkflowDraftSlice } from './workflow-draft-slice'
import type { WorkflowSliceShape } from './workflow-slice'
import { createWorkflowSlice } from './workflow-slice'
import { WorkflowContext } from '@/app/components/workflow/context'
import type { WorkflowSliceShape as WorkflowAppSliceShape } from '@/app/components/workflow-app/store/workflow/workflow-slice'

export type Shape =
ChatVariableSliceShape &
@@ -38,9 +42,16 @@ export type Shape =
ToolSliceShape &
VersionSliceShape &
WorkflowDraftSliceShape &
WorkflowSliceShape
WorkflowSliceShape &
WorkflowAppSliceShape

type CreateWorkflowStoreParams = {
injectWorkflowStoreSliceFn?: StateCreator<WorkflowAppSliceShape>
}

export const createWorkflowStore = (params: CreateWorkflowStoreParams) => {
const { injectWorkflowStoreSliceFn } = params || {}

export const createWorkflowStore = () => {
return createStore<Shape>((...args) => ({
...createChatVariableSlice(...args),
...createEnvVariableSlice(...args),
@@ -53,6 +64,7 @@ export const createWorkflowStore = () => {
...createVersionSlice(...args),
...createWorkflowDraftSlice(...args),
...createWorkflowSlice(...args),
...(injectWorkflowStoreSliceFn?.(...args) || {} as WorkflowAppSliceShape),
}))
}


+ 0
- 4
web/app/components/workflow/store/workflow/node-slice.ts Ver arquivo

@@ -12,8 +12,6 @@ import type {
export type NodeSliceShape = {
showSingleRunPanel: boolean
setShowSingleRunPanel: (showSingleRunPanel: boolean) => void
nodesDefaultConfigs: Record<string, any>
setNodesDefaultConfigs: (nodesDefaultConfigs: Record<string, any>) => void
nodeAnimation: boolean
setNodeAnimation: (nodeAnimation: boolean) => void
candidateNode?: Node
@@ -55,8 +53,6 @@ export type NodeSliceShape = {
export const createNodeSlice: StateCreator<NodeSliceShape> = set => ({
showSingleRunPanel: false,
setShowSingleRunPanel: showSingleRunPanel => set(() => ({ showSingleRunPanel })),
nodesDefaultConfigs: {},
setNodesDefaultConfigs: nodesDefaultConfigs => set(() => ({ nodesDefaultConfigs })),
nodeAnimation: false,
setNodeAnimation: nodeAnimation => set(() => ({ nodeAnimation })),
candidateNode: undefined,

+ 4
- 6
web/app/components/workflow/store/workflow/workflow-slice.ts Ver arquivo

@@ -10,11 +10,8 @@ type PreviewRunningData = WorkflowRunningData & {
}

export type WorkflowSliceShape = {
appId: string
workflowRunningData?: PreviewRunningData
setWorkflowRunningData: (workflowData: PreviewRunningData) => void
notInitialWorkflow: boolean
setNotInitialWorkflow: (notInitialWorkflow: boolean) => void
clipboardElements: Node[]
setClipboardElements: (clipboardElements: Node[]) => void
selection: null | { x1: number; y1: number; x2: number; y2: number }
@@ -33,14 +30,13 @@ export type WorkflowSliceShape = {
setShowImportDSLModal: (showImportDSLModal: boolean) => void
showTips: string
setShowTips: (showTips: string) => void
workflowConfig?: Record<string, any>
setWorkflowConfig: (workflowConfig: Record<string, any>) => void
}

export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
appId: '',
workflowRunningData: undefined,
setWorkflowRunningData: workflowRunningData => set(() => ({ workflowRunningData })),
notInitialWorkflow: false,
setNotInitialWorkflow: notInitialWorkflow => set(() => ({ notInitialWorkflow })),
clipboardElements: [],
setClipboardElements: clipboardElements => set(() => ({ clipboardElements })),
selection: null,
@@ -62,4 +58,6 @@ export const createWorkflowSlice: StateCreator<WorkflowSliceShape> = set => ({
setShowImportDSLModal: showImportDSLModal => set(() => ({ showImportDSLModal })),
showTips: '',
setShowTips: showTips => set(() => ({ showTips })),
workflowConfig: undefined,
setWorkflowConfig: workflowConfig => set(() => ({ workflowConfig })),
})

+ 6
- 2
web/service/use-workflow.ts Ver arquivo

@@ -21,10 +21,14 @@ export const useAppWorkflow = (appID: string) => {
})
}

export const useWorkflowConfig = (appId: string) => {
export const useWorkflowConfig = (appId: string, onSuccess: (v: WorkflowConfigResponse) => void) => {
return useQuery({
queryKey: [NAME_SPACE, 'config', appId],
queryFn: () => get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`),
queryFn: async () => {
const data = await get<WorkflowConfigResponse>(`/apps/${appId}/workflows/draft/config`)
onSuccess(data)
return data
},
})
}


Carregando…
Cancelar
Salvar