fix(workflow): disable view switch during preview run instead of mounted guard

Simpler approach: disable the view picker toggle when preview is running,
preventing users from switching views during active runs.

This replaces the previous mounted ref guard approach (commits a0188bd9b5,
b7f1eb9b7b, 8332f0de2b) which added complexity to handle post-unmount
operations. Disabling the toggle is more direct and follows KISS principle.

Changes:
- Add disabled prop to ViewPicker based on isResponding state
- Revert mounted ref guards in use-chat-flow-control.ts
- Revert isMountedRef parameter in use-nodes/edges-interactions-without-sync.ts
- Revert defensive type check in markdown-utils.ts (no longer needed)
This commit is contained in:
yyh
2026-01-27 01:31:22 +08:00
parent 8332f0de2b
commit 772dbe620d
6 changed files with 18 additions and 40 deletions

View File

@ -8,7 +8,7 @@ import { ALLOW_UNSAFE_DATA_SCHEME } from '@/config'
export const preprocessLaTeX = (content: string) => {
if (typeof content !== 'string')
return ''
return content
const codeBlockRegex = /```[\s\S]*?```/g
const codeBlocks = content.match(codeBlockRegex) || []
@ -32,9 +32,6 @@ export const preprocessLaTeX = (content: string) => {
}
export const preprocessThinkTag = (content: string) => {
if (typeof content !== 'string')
return ''
const thinkOpenTagRegex = /(<think>\s*)+/g
const thinkCloseTagRegex = /(\s*<\/think>)+/g
return flow([

View File

@ -23,7 +23,7 @@ import {
WorkflowContextProvider,
} from '@/app/components/workflow/context'
import { HeaderShell } from '@/app/components/workflow/header'
import { useWorkflowStore } from '@/app/components/workflow/store'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { useTriggerStatusStore } from '@/app/components/workflow/store/trigger-status'
import {
SupportUploadFileTypes,
@ -63,6 +63,7 @@ const WorkflowViewContent = ({
}: WorkflowViewContentProps) => {
const features = useFeatures(s => s.features)
const isSupportSandbox = !!features.sandbox?.enabled
const isResponding = useStore(s => s.isResponding)
const [viewType, doSetViewType] = useQueryState(WORKFLOW_VIEW_PARAM_KEY, parseAsViewType)
const { syncWorkflowDraftImmediately } = useNodesSyncDraft()
const pendingSyncRef = useRef<Promise<void> | null>(null)
@ -101,6 +102,7 @@ const WorkflowViewContent = ({
<ViewPicker
value={viewType}
onChange={handleViewTypeChange}
disabled={isResponding}
/>
)
const viewPickerDock = (

View File

@ -1,15 +1,11 @@
import type { RefObject } from 'react'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
export const useEdgesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean>) => {
export const useEdgesInteractionsWithoutSync = () => {
const store = useStoreApi()
const handleEdgeCancelRunningStatus = useCallback(() => {
if (isMountedRef && isMountedRef.current === false)
return
const {
edges,
setEdges,
@ -23,7 +19,7 @@ export const useEdgesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean
})
})
setEdges(newEdges)
}, [store, isMountedRef])
}, [store])
return {
handleEdgeCancelRunningStatus,

View File

@ -1,16 +1,12 @@
import type { RefObject } from 'react'
import { produce } from 'immer'
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import { NodeRunningStatus } from '../types'
export const useNodesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean>) => {
export const useNodesInteractionsWithoutSync = () => {
const store = useStoreApi()
const handleNodeCancelRunningStatus = useCallback(() => {
if (isMountedRef && isMountedRef.current === false)
return
const {
getNodes,
setNodes,
@ -24,12 +20,9 @@ export const useNodesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean
})
})
setNodes(newNodes)
}, [store, isMountedRef])
}, [store])
const handleCancelAllNodeSuccessStatus = useCallback(() => {
if (isMountedRef && isMountedRef.current === false)
return
const {
getNodes,
setNodes,
@ -43,12 +36,9 @@ export const useNodesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean
})
})
setNodes(newNodes)
}, [store, isMountedRef])
}, [store])
const handleCancelNodeSuccessStatus = useCallback((nodeId: string) => {
if (isMountedRef && isMountedRef.current === false)
return
const {
getNodes,
setNodes,
@ -62,7 +52,7 @@ export const useNodesInteractionsWithoutSync = (isMountedRef?: RefObject<boolean
}
})
setNodes(newNodes)
}, [store, isMountedRef])
}, [store])
return {
handleNodeCancelRunningStatus,

View File

@ -1,4 +1,4 @@
import { useCallback, useEffect, useRef } from 'react'
import { useCallback } from 'react'
import { DEFAULT_ITER_TIMES, DEFAULT_LOOP_TIMES } from '../../../constants'
import { useEdgesInteractionsWithoutSync } from '../../../hooks/use-edges-interactions-without-sync'
import { useNodesInteractionsWithoutSync } from '../../../hooks/use-nodes-interactions-without-sync'
@ -19,17 +19,8 @@ export function useChatFlowControl({
const setHasStopResponded = useStore(s => s.setHasStopResponded)
const setSuggestedQuestionsAbortController = useStore(s => s.setSuggestedQuestionsAbortController)
const invalidateRun = useStore(s => s.invalidateRun)
const isMountedRef = useRef(true)
useEffect(() => {
isMountedRef.current = true
return () => {
isMountedRef.current = false
}
}, [])
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync(isMountedRef)
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync(isMountedRef)
const { handleNodeCancelRunningStatus } = useNodesInteractionsWithoutSync()
const { handleEdgeCancelRunningStatus } = useEdgesInteractionsWithoutSync()
const { setIterTimes, setLoopTimes } = workflowStore.getState()

View File

@ -12,18 +12,20 @@ type ViewPickerProps = {
value: ViewType
onChange: (value: ViewType) => void
className?: string
disabled?: boolean
}
const ViewPicker: FC<ViewPickerProps> = ({
value,
onChange,
className,
disabled,
}) => {
const { t } = useTranslation()
const options = useMemo(() => ([
{ value: ViewType.graph, text: t('viewPicker.graph', { ns: 'workflow' }) },
{ value: ViewType.skill, text: t('viewPicker.skill', { ns: 'workflow' }) },
]), [t])
{ value: ViewType.graph, text: t('viewPicker.graph', { ns: 'workflow' }), disabled: disabled && value !== ViewType.graph },
{ value: ViewType.skill, text: t('viewPicker.skill', { ns: 'workflow' }), disabled: disabled && value !== ViewType.skill },
]), [t, disabled, value])
const handleChange = useCallback((nextValue: string | number | symbol) => {
if (nextValue === value)