diff --git a/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx b/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx index 2c23a05..e0d3403 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-editor-provider.tsx @@ -20,25 +20,16 @@ import { syncConditionBranchTargetsFromEdges, } from "./workflow-utils" -export type SelectedWorkflowBranch = { - nodeId: string - branchId: string -} - export function useFlowgramEditorProps({ definition, nodeSpecs, - selectedBranch, readonly = false, onDefinitionChange, - onSelectBranch, }: { definition: AIWorkflowDefinition nodeSpecs: AIWorkflowNodeSpec[] - selectedBranch?: SelectedWorkflowBranch | null readonly?: boolean onDefinitionChange?: (definition: AIWorkflowDefinition) => void - onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void }) { return useMemo( () => { @@ -59,7 +50,7 @@ export function useFlowgramEditorProps({ materials: { renderDefaultNode: FlowgramNodeRenderer, }, - nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs, selectedBranch, onSelectBranch), + nodeRegistries: buildFlowgramNodeRegistries(nodeSpecs), nodeEngine: { enable: true, }, @@ -123,7 +114,7 @@ export function useFlowgramEditorProps({ ], } }, - [definition, nodeSpecs, onDefinitionChange, onSelectBranch, readonly, selectedBranch] + [definition, nodeSpecs, onDefinitionChange, readonly] ) } diff --git a/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx b/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx index 43cf310..5d09724 100644 --- a/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx +++ b/web/app/dashboard/ai-workflows/_components/flowgram-node-registries.tsx @@ -8,13 +8,9 @@ import { normalizeNodeConfig, type WorkflowConditionBranch, } from "./workflow-utils" -import type { SelectedWorkflowBranch } from "./flowgram-editor-provider" +import { useWorkflowBranchSelection } from "./workflow-branch-selection" -export function buildFlowgramNodeRegistries( - nodeSpecs: AIWorkflowNodeSpec[], - selectedBranch?: SelectedWorkflowBranch | null, - onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void -): WorkflowNodeRegistry[] { +export function buildFlowgramNodeRegistries(nodeSpecs: AIWorkflowNodeSpec[]): WorkflowNodeRegistry[] { const seen = new Set() const specs = nodeSpecs.length > 0 ? nodeSpecs @@ -45,8 +41,6 @@ export function buildFlowgramNodeRegistries( ), }, @@ -56,13 +50,9 @@ export function buildFlowgramNodeRegistries( function FlowgramNodeForm({ nodeType, fallbackTitle, - selectedBranch, - onSelectBranch, }: { nodeType: string fallbackTitle: string - selectedBranch?: SelectedWorkflowBranch | null - onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void }) { const { node } = useNodeRender() const nodeId = String(node.id ?? "") @@ -72,8 +62,6 @@ function FlowgramNodeForm({ ) } @@ -108,14 +96,12 @@ function defaultPortsForNodeType(type: string) { function ConditionNodeForm({ fallbackTitle, nodeId, - selectedBranch, - onSelectBranch, }: { fallbackTitle: string nodeId: string - selectedBranch?: SelectedWorkflowBranch | null - onSelectBranch?: (branch: SelectedWorkflowBranch | null) => void }) { + const { selectedBranch, onSelectBranch } = useWorkflowBranchSelection() + return (
name="title"> @@ -154,10 +140,16 @@ function ConditionNodeForm({ ? "bg-[#dfe4ff] text-[#3327b9] ring-1 ring-inset ring-[#4e40e5]/45 before:w-1" : "bg-[#eef1f7] hover:bg-[#e6eaff]", ].join(" ")} - onClick={(event) => { + onPointerDownCapture={(event) => { event.stopPropagation() onSelectBranch?.({ nodeId, branchId: branch.id }) }} + onMouseDownCapture={(event) => { + event.stopPropagation() + }} + onClick={(event) => { + event.stopPropagation() + }} > {branch.name || (branch.default ? "默认分支" : branch.id)} @@ -170,6 +162,12 @@ function ConditionNodeForm({ type="button" className="flex size-5 shrink-0 items-center justify-center rounded-sm text-muted-foreground transition-colors hover:bg-destructive/10 hover:text-destructive" aria-label={`删除条件 ${branch.name || branch.id}`} + onPointerDownCapture={(event) => { + event.stopPropagation() + }} + onMouseDownCapture={(event) => { + event.stopPropagation() + }} onClick={(event) => { event.stopPropagation() deleteBranch(branch.id) diff --git a/web/app/dashboard/ai-workflows/_components/workflow-branch-selection.tsx b/web/app/dashboard/ai-workflows/_components/workflow-branch-selection.tsx new file mode 100644 index 0000000..c128db0 --- /dev/null +++ b/web/app/dashboard/ai-workflows/_components/workflow-branch-selection.tsx @@ -0,0 +1,41 @@ +"use client" + +import { createContext, useContext, useMemo, type ReactNode } from "react" + +export type SelectedWorkflowBranch = { + nodeId: string + branchId: string +} + +type WorkflowBranchSelectionContextValue = { + selectedBranch: SelectedWorkflowBranch | null + onSelectBranch: (branch: SelectedWorkflowBranch | null) => void +} + +const WorkflowBranchSelectionContext = createContext({ + selectedBranch: null, + onSelectBranch: () => {}, +}) + +export function WorkflowBranchSelectionProvider({ + selectedBranch, + onSelectBranch, + children, +}: WorkflowBranchSelectionContextValue & { + children: ReactNode +}) { + const value = useMemo( + () => ({ selectedBranch, onSelectBranch }), + [onSelectBranch, selectedBranch] + ) + + return ( + + {children} + + ) +} + +export function useWorkflowBranchSelection() { + return useContext(WorkflowBranchSelectionContext) +} diff --git a/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx b/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx index 9583632..beec1d4 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-config-sidebar.tsx @@ -5,7 +5,7 @@ import { XIcon } from "lucide-react" import { Button } from "@/components/ui/button" import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" -import type { SelectedWorkflowBranch } from "./flowgram-editor-provider" +import type { SelectedWorkflowBranch } from "./workflow-branch-selection" import { ConditionBranchConfigPanel, NodeConfigPanel } from "./node-config-panel" import { getAvailableVariables, diff --git a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx index f9cce9c..80ab1f9 100644 --- a/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx +++ b/web/app/dashboard/ai-workflows/_components/workflow-editor.tsx @@ -1,6 +1,6 @@ "use client" -import { useEffect, useMemo, useState, type ReactNode } from "react" +import { useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react" import { EditorRenderer, @@ -16,7 +16,11 @@ import { import type { AIWorkflowDefinition, AIWorkflowNodeSpec } from "@/lib/api/admin" -import { useFlowgramEditorProps, type SelectedWorkflowBranch } from "./flowgram-editor-provider" +import { useFlowgramEditorProps } from "./flowgram-editor-provider" +import { + WorkflowBranchSelectionProvider, + type SelectedWorkflowBranch, +} from "./workflow-branch-selection" import { WorkflowConfigPanel } from "./workflow-config-sidebar" import { WorkflowEditorToolbar } from "./workflow-editor-toolbar" import { WorkflowNodePalette } from "./workflow-node-palette" @@ -66,6 +70,7 @@ export function WorkflowEditor({ const [localDefinition, setLocalDefinition] = useState(definition) const [selectedNodeId, setSelectedNodeId] = useState("") const [selectedBranch, setSelectedBranch] = useState(null) + const branchSelectAtRef = useRef(0) const validation = useMemo( () => validateWorkflowDefinition(localDefinition, nodeSpecs), @@ -75,53 +80,64 @@ export function WorkflowEditor({ const editorProps = useFlowgramEditorProps({ definition: localDefinition, nodeSpecs, - selectedBranch, onDefinitionChange: (next) => { setLocalDefinition(next) onDefinitionChange(next) }, - onSelectBranch: (branch) => { + }) + + const handleSelectBranch = useCallback( + (branch: SelectedWorkflowBranch | null) => { if (!branch) { setSelectedBranch(null) return } + branchSelectAtRef.current = Date.now() setSelectedNodeId(branch.nodeId) setSelectedBranch(branch) }, - }) + [] + ) return ( - - { - setLocalDefinition(next) - onDefinitionChange(next) - }} - onSelectNode={(nodeId) => { - setSelectedNodeId(nodeId) - setSelectedBranch(null) - }} - onSelectBranch={setSelectedBranch} - onUndo={onUndo} - undoDisabled={undoDisabled} - onRedo={onRedo} - redoDisabled={redoDisabled} - onRestoreDefault={onRestoreDefault} - restoreDefaultDisabled={restoreDefaultDisabled} - onValidate={onValidate} - validateDisabled={validateDisabled} - onSaveDraft={onSaveDraft} - saveDraftDisabled={saveDraftDisabled} - onPublish={onPublish} - publishDisabled={publishDisabled} - /> - + + + { + setLocalDefinition(next) + onDefinitionChange(next) + }} + onSelectNode={(nodeId) => { + setSelectedNodeId(nodeId) + if (!nodeId || Date.now() - branchSelectAtRef.current > 160) { + setSelectedBranch(null) + } + }} + onSelectBranch={handleSelectBranch} + onUndo={onUndo} + undoDisabled={undoDisabled} + onRedo={onRedo} + redoDisabled={redoDisabled} + onRestoreDefault={onRestoreDefault} + restoreDefaultDisabled={restoreDefaultDisabled} + onValidate={onValidate} + validateDisabled={validateDisabled} + onSaveDraft={onSaveDraft} + saveDraftDisabled={saveDraftDisabled} + onPublish={onPublish} + publishDisabled={publishDisabled} + /> + + ) }