| const { formatTimeFromNow } = useWorkflow() | const { formatTimeFromNow } = useWorkflow() | ||||
| const draftUpdatedAt = useStore(state => state.draftUpdatedAt) | const draftUpdatedAt = useStore(state => state.draftUpdatedAt) | ||||
| const publishedAt = useStore(state => state.publishedAt) | const publishedAt = useStore(state => state.publishedAt) | ||||
| const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft) | |||||
| return ( | return ( | ||||
| <div className='flex items-center h-[18px] text-xs text-gray-500'> | <div className='flex items-center h-[18px] text-xs text-gray-500'> | ||||
| ? `${t('workflow.common.published')} ${formatTimeFromNow(publishedAt)}` | ? `${t('workflow.common.published')} ${formatTimeFromNow(publishedAt)}` | ||||
| : t('workflow.common.unpublished') | : t('workflow.common.unpublished') | ||||
| } | } | ||||
| { | |||||
| isSyncingWorkflowDraft && ( | |||||
| <> | |||||
| <span className='flex items-center mx-1'>·</span> | |||||
| {t('workflow.common.syncingData')} | |||||
| </> | |||||
| ) | |||||
| } | |||||
| </div> | </div> | ||||
| ) | ) | ||||
| } | } |
| const { | const { | ||||
| appId, | appId, | ||||
| setSyncWorkflowDraftHash, | setSyncWorkflowDraftHash, | ||||
| setIsSyncingWorkflowDraft, | |||||
| } = workflowStore.getState() | } = workflowStore.getState() | ||||
| setIsSyncingWorkflowDraft(true) | |||||
| fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => { | fetchWorkflowDraft(`/apps/${appId}/workflows/draft`).then((response) => { | ||||
| handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdator) | handleUpdateWorkflowCanvas(response.graph as WorkflowDataUpdator) | ||||
| setSyncWorkflowDraftHash(response.hash) | setSyncWorkflowDraftHash(response.hash) | ||||
| }) | |||||
| }).finally(() => setIsSyncingWorkflowDraft(false)) | |||||
| }, [handleUpdateWorkflowCanvas, workflowStore]) | }, [handleUpdateWorkflowCanvas, workflowStore]) | ||||
| return { | return { |
| import CandidateNode from './candidate-node' | import CandidateNode from './candidate-node' | ||||
| import PanelContextmenu from './panel-contextmenu' | import PanelContextmenu from './panel-contextmenu' | ||||
| import NodeContextmenu from './node-contextmenu' | import NodeContextmenu from './node-contextmenu' | ||||
| import SyncingDataModal from './syncing-data-modal' | |||||
| import { | import { | ||||
| useStore, | useStore, | ||||
| useWorkflowStore, | useWorkflowStore, | ||||
| const controlMode = useStore(s => s.controlMode) | const controlMode = useStore(s => s.controlMode) | ||||
| const nodeAnimation = useStore(s => s.nodeAnimation) | const nodeAnimation = useStore(s => s.nodeAnimation) | ||||
| const showConfirm = useStore(s => s.showConfirm) | const showConfirm = useStore(s => s.showConfirm) | ||||
| const { setShowConfirm } = workflowStore.getState() | |||||
| const { | |||||
| setShowConfirm, | |||||
| setControlPromptEditorRerenderKey, | |||||
| } = workflowStore.getState() | |||||
| const { | const { | ||||
| handleSyncWorkflowDraft, | handleSyncWorkflowDraft, | ||||
| syncWorkflowDraftWhenPageClose, | syncWorkflowDraftWhenPageClose, | ||||
| if (v.type === WORKFLOW_DATA_UPDATE) { | if (v.type === WORKFLOW_DATA_UPDATE) { | ||||
| setNodes(v.payload.nodes) | setNodes(v.payload.nodes) | ||||
| setEdges(v.payload.edges) | setEdges(v.payload.edges) | ||||
| setTimeout(() => setControlPromptEditorRerenderKey(Date.now())) | |||||
| } | } | ||||
| }) | }) | ||||
| if (document.visibilityState === 'hidden') | if (document.visibilityState === 'hidden') | ||||
| syncWorkflowDraftWhenPageClose() | syncWorkflowDraftWhenPageClose() | ||||
| else if (document.visibilityState === 'visible') | else if (document.visibilityState === 'visible') | ||||
| handleRefreshWorkflowDraft() | |||||
| setTimeout(() => handleRefreshWorkflowDraft(), 500) | |||||
| }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft]) | }, [syncWorkflowDraftWhenPageClose, handleRefreshWorkflowDraft]) | ||||
| useEffect(() => { | useEffect(() => { | ||||
| `} | `} | ||||
| ref={workflowContainerRef} | ref={workflowContainerRef} | ||||
| > | > | ||||
| <SyncingDataModal /> | |||||
| <CandidateNode /> | <CandidateNode /> | ||||
| <Header /> | <Header /> | ||||
| <Panel /> | <Panel /> |
| import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars' | import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor/editor-support-vars' | ||||
| import Switch from '@/app/components/base/switch' | import Switch from '@/app/components/base/switch' | ||||
| import { Jinja } from '@/app/components/base/icons/src/vender/workflow' | import { Jinja } from '@/app/components/base/icons/src/vender/workflow' | ||||
| import { useStore } from '@/app/components/workflow/store' | |||||
| type Props = { | type Props = { | ||||
| className?: string | className?: string | ||||
| }) => { | }) => { | ||||
| const { t } = useTranslation() | const { t } = useTranslation() | ||||
| const { eventEmitter } = useEventEmitterContextContext() | const { eventEmitter } = useEventEmitterContextContext() | ||||
| const controlPromptEditorRerenderKey = useStore(s => s.controlPromptEditorRerenderKey) | |||||
| const isShowHistory = !isChatModel && isChatApp | const isShowHistory = !isChatModel && isChatApp | ||||
| ? ( | ? ( | ||||
| <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}> | <div className={cn(isExpand ? 'grow' : 'max-h-[536px]', 'relative px-3 min-h-[56px] overflow-y-auto')}> | ||||
| <PromptEditor | <PromptEditor | ||||
| key={controlPromptEditorRerenderKey} | |||||
| instanceId={instanceId} | instanceId={instanceId} | ||||
| compact | compact | ||||
| className='min-h-[56px]' | className='min-h-[56px]' |
| nodeData: VariableAssignerNodeType | nodeData: VariableAssignerNodeType | ||||
| } | } | ||||
| setEnteringNodePayload: (enteringNodePayload?: Shape['enteringNodePayload']) => void | setEnteringNodePayload: (enteringNodePayload?: Shape['enteringNodePayload']) => void | ||||
| isSyncingWorkflowDraft: boolean | |||||
| setIsSyncingWorkflowDraft: (isSyncingWorkflowDraft: boolean) => void | |||||
| controlPromptEditorRerenderKey: number | |||||
| setControlPromptEditorRerenderKey: (controlPromptEditorRerenderKey: number) => void | |||||
| } | } | ||||
| export const createWorkflowStore = () => { | export const createWorkflowStore = () => { | ||||
| setConnectingNodePayload: connectingNodePayload => set(() => ({ connectingNodePayload })), | setConnectingNodePayload: connectingNodePayload => set(() => ({ connectingNodePayload })), | ||||
| enteringNodePayload: undefined, | enteringNodePayload: undefined, | ||||
| setEnteringNodePayload: enteringNodePayload => set(() => ({ enteringNodePayload })), | setEnteringNodePayload: enteringNodePayload => set(() => ({ enteringNodePayload })), | ||||
| isSyncingWorkflowDraft: false, | |||||
| setIsSyncingWorkflowDraft: isSyncingWorkflowDraft => set(() => ({ isSyncingWorkflowDraft })), | |||||
| controlPromptEditorRerenderKey: 0, | |||||
| setControlPromptEditorRerenderKey: controlPromptEditorRerenderKey => set(() => ({ controlPromptEditorRerenderKey })), | |||||
| })) | })) | ||||
| } | } | ||||
| import { useStore } from './store' | |||||
| const SyncingDataModal = () => { | |||||
| const isSyncingWorkflowDraft = useStore(s => s.isSyncingWorkflowDraft) | |||||
| if (!isSyncingWorkflowDraft) | |||||
| return null | |||||
| return ( | |||||
| <div className='absolute inset-0 z-[9999]'> | |||||
| </div> | |||||
| ) | |||||
| } | |||||
| export default SyncingDataModal |
| manageInTools: 'Manage in Tools', | manageInTools: 'Manage in Tools', | ||||
| workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.', | workflowAsToolTip: 'Tool reconfiguration is required after the workflow update.', | ||||
| viewDetailInTracingPanel: 'View details', | viewDetailInTracingPanel: 'View details', | ||||
| syncingData: 'Syncing data, just a few seconds.', | |||||
| }, | }, | ||||
| errorMsg: { | errorMsg: { | ||||
| fieldRequired: '{{field}} is required', | fieldRequired: '{{field}} is required', |
| manageInTools: '访问工具页', | manageInTools: '访问工具页', | ||||
| workflowAsToolTip: '工作流更新后需要重新配置工具参数', | workflowAsToolTip: '工作流更新后需要重新配置工具参数', | ||||
| viewDetailInTracingPanel: '查看详细信息', | viewDetailInTracingPanel: '查看详细信息', | ||||
| syncingData: '同步数据中,只需几秒钟。', | |||||
| }, | }, | ||||
| errorMsg: { | errorMsg: { | ||||
| fieldRequired: '{{field}} 不能为空', | fieldRequired: '{{field}} 不能为空', |