Merge upstream/feat/vibe-wf: keep clickOutsideNotClose and z-index fix

This commit is contained in:
GuanMu 2025-12-29 14:33:42 +08:00
commit 88148f7a27
4 changed files with 168 additions and 78 deletions

View File

@ -10,9 +10,15 @@ type VersionSelectorProps = {
versionLen: number versionLen: number
value: number value: number
onChange: (index: number) => void onChange: (index: number) => void
contentClassName?: string
} }
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => { const VersionSelector: React.FC<VersionSelectorProps> = ({
versionLen,
value,
onChange,
contentClassName,
}) => {
const { t } = useTranslation() const { t } = useTranslation()
const [isOpen, { const [isOpen, {
setFalse: handleOpenFalse, setFalse: handleOpenFalse,
@ -64,6 +70,7 @@ const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, on
</PortalToFollowElemTrigger> </PortalToFollowElemTrigger>
<PortalToFollowElemContent className={cn( <PortalToFollowElemContent className={cn(
'z-[99]', 'z-[99]',
contentClassName,
)} )}
> >
<div <div

View File

@ -1,10 +1,10 @@
'use client' 'use client'
import type { ToolDefaultValue } from '../block-selector/types' import type { ToolDefaultValue } from '../block-selector/types'
import type { FlowGraph } from '../store/workflow/vibe-workflow-slice'
import type { Edge, Node, ToolWithProvider } from '../types' import type { Edge, Node, ToolWithProvider } from '../types'
import type { Tool } from '@/app/components/tools/types' import type { Tool } from '@/app/components/tools/types'
import type { Model } from '@/types/app' import type { Model } from '@/types/app'
import { useSessionStorageState } from 'ahooks'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next' import { useTranslation } from 'react-i18next'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
@ -30,7 +30,7 @@ import {
VIBE_COMMAND_EVENT, VIBE_COMMAND_EVENT,
} from '../constants' } from '../constants'
import { useHooksStore } from '../hooks-store' import { useHooksStore } from '../hooks-store'
import { useWorkflowStore } from '../store' import { useStore, useWorkflowStore } from '../store'
import { BlockEnum } from '../types' import { BlockEnum } from '../types'
import { import {
generateNewNode, generateNewNode,
@ -77,11 +77,6 @@ type ParseResult = {
edges: ParsedEdge[] edges: ParsedEdge[]
} }
type FlowGraph = {
nodes: Node[]
edges: Edge[]
}
const NODE_DECLARATION = /^([A-Z][\w-]*)\s*\[(?:"([^"]+)"|([^\]]+))\]\s*$/i const NODE_DECLARATION = /^([A-Z][\w-]*)\s*\[(?:"([^"]+)"|([^\]]+))\]\s*$/i
const EDGE_DECLARATION = /^(.+?)\s*-->\s*(?:\|([^|]+)\|\s*)?(.+)$/ const EDGE_DECLARATION = /^(.+?)\s*-->\s*(?:\|([^|]+)\|\s*)?(.+)$/
@ -282,67 +277,92 @@ const buildToolParams = (parameters?: Tool['parameters']) => {
return params return params
} }
type UseVibeFlowDataParams = { // Sync vibe flow data to sessionStorage
storageKey: string const STORAGE_KEY_PREFIX = 'vibe-flow-'
const loadFromSessionStorage = (flowId: string): { versions: FlowGraph[], currentIndex: number } | null => {
if (typeof window === 'undefined')
return null
try {
const versionsKey = `${STORAGE_KEY_PREFIX}${flowId}-versions`
const indexKey = `${STORAGE_KEY_PREFIX}${flowId}-version-index`
const versionsRaw = sessionStorage.getItem(versionsKey)
const indexRaw = sessionStorage.getItem(indexKey)
if (!versionsRaw)
return null
const versions = JSON.parse(versionsRaw) as FlowGraph[]
const currentIndex = indexRaw ? Number.parseInt(indexRaw, 10) : 0
return { versions, currentIndex }
}
catch {
return null
}
} }
const keyPrefix = 'vibe-flow-' const saveToSessionStorage = (flowId: string, versions: FlowGraph[], currentIndex: number) => {
if (typeof window === 'undefined')
return
export const useVibeFlowData = ({ storageKey }: UseVibeFlowDataParams) => { try {
const [versions, setVersions] = useSessionStorageState<FlowGraph[]>(`${keyPrefix}${storageKey}-versions`, { const versionsKey = `${STORAGE_KEY_PREFIX}${flowId}-versions`
defaultValue: [], const indexKey = `${STORAGE_KEY_PREFIX}${flowId}-version-index`
})
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, { sessionStorage.setItem(versionsKey, JSON.stringify(versions))
defaultValue: 0, sessionStorage.setItem(indexKey, String(currentIndex))
})
useEffect(() => {
if (!versions || versions.length === 0) {
if (currentVersionIndex !== 0 && currentVersionIndex !== -1)
setCurrentVersionIndex(0)
return
}
if (currentVersionIndex === -1)
return
const normalizedIndex = Math.min(Math.max(currentVersionIndex ?? 0, 0), versions.length - 1)
if (normalizedIndex !== currentVersionIndex)
setCurrentVersionIndex(normalizedIndex)
}, [versions, currentVersionIndex, setCurrentVersionIndex])
const current = useMemo(() => {
if (!versions || versions.length === 0)
return undefined
const index = currentVersionIndex ?? 0
if (index < 0)
return undefined
return versions[index] || versions[versions.length - 1]
}, [versions, currentVersionIndex])
const addVersion = useCallback((version: FlowGraph) => {
// Prevent adding empty graphs
if (!version || !version.nodes || version.nodes.length === 0) {
setCurrentVersionIndex(-1)
return
}
setVersions((prev) => {
const newVersions = [...(prev || []), version]
// Set index in setVersions callback to ensure using the latest length
setCurrentVersionIndex(newVersions.length - 1)
return newVersions
})
}, [setVersions, setCurrentVersionIndex])
return {
versions,
addVersion,
currentVersionIndex,
setCurrentVersionIndex,
current,
} }
catch (error) {
console.error('Failed to save vibe flow to sessionStorage:', error)
}
}
export const useVibeFlowSessionStorage = (flowId: string) => {
const workflowStore = useWorkflowStore()
const versions = useStore(s => s.vibeFlowVersions)
const currentIndex = useStore(s => s.vibeFlowCurrentIndex)
const loadedFlowIdRef = useRef<string | null>(null)
const isLoadingRef = useRef(false)
// Load from sessionStorage when flowId changes
useEffect(() => {
if (!flowId || loadedFlowIdRef.current === flowId)
return
isLoadingRef.current = true
const stored = loadFromSessionStorage(flowId)
if (stored) {
workflowStore.setState({
vibeFlowVersions: stored.versions,
vibeFlowCurrentIndex: stored.currentIndex,
})
}
else {
workflowStore.setState({
vibeFlowVersions: [],
vibeFlowCurrentIndex: 0,
currentVibeFlow: undefined,
})
}
loadedFlowIdRef.current = flowId
// Delay to prevent immediate save
setTimeout(() => {
isLoadingRef.current = false
}, 100)
}, [flowId, workflowStore])
// Save to sessionStorage when versions or index change
useEffect(() => {
if (!flowId || loadedFlowIdRef.current !== flowId || isLoadingRef.current)
return
saveToSessionStorage(flowId, versions, currentIndex)
}, [flowId, versions, currentIndex])
} }
export const useWorkflowVibe = () => { export const useWorkflowVibe = () => {
@ -367,9 +387,7 @@ export const useWorkflowVibe = () => {
const isGeneratingRef = useRef(false) const isGeneratingRef = useRef(false)
const lastInstructionRef = useRef<string>('') const lastInstructionRef = useRef<string>('')
const { addVersion, current: currentFlowGraph } = useVibeFlowData({ useVibeFlowSessionStorage(configsMap?.flowId || '')
storageKey: configsMap?.flowId || '',
})
useEffect(() => { useEffect(() => {
const storedModel = (() => { const storedModel = (() => {
@ -702,6 +720,8 @@ export const useWorkflowVibe = () => {
}, [nodeTypeLookup, toolLookup]) }, [nodeTypeLookup, toolLookup])
const applyFlowchartToWorkflow = useCallback(() => { const applyFlowchartToWorkflow = useCallback(() => {
const currentFlowGraph = workflowStore.getState().currentVibeFlow
if (!currentFlowGraph || !currentFlowGraph.nodes || currentFlowGraph.nodes.length === 0) { if (!currentFlowGraph || !currentFlowGraph.nodes || currentFlowGraph.nodes.length === 0) {
Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') })
return return
@ -722,7 +742,6 @@ export const useWorkflowVibe = () => {
vibePanelMermaidCode: '', vibePanelMermaidCode: '',
})) }))
}, [ }, [
currentFlowGraph,
handleSyncWorkflowDraft, handleSyncWorkflowDraft,
nodeTypeLookup, nodeTypeLookup,
nodesMetaDataMap, nodesMetaDataMap,
@ -730,6 +749,7 @@ export const useWorkflowVibe = () => {
store, store,
t, t,
toolLookup, toolLookup,
workflowStore,
]) ])
const handleVibeCommand = useCallback(async (dsl?: string, skipPanelPreview = false) => { const handleVibeCommand = useCallback(async (dsl?: string, skipPanelPreview = false) => {
@ -830,7 +850,7 @@ export const useWorkflowVibe = () => {
})) }))
const workflowGraph = await flowchartToWorkflowGraph(mermaidCode) const workflowGraph = await flowchartToWorkflowGraph(mermaidCode)
addVersion(workflowGraph) workflowStore.getState().addVibeFlowVersion(workflowGraph)
if (skipPanelPreview) if (skipPanelPreview)
applyFlowchartToWorkflow() applyFlowchartToWorkflow()

View File

@ -20,8 +20,6 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import { ModelModeType } from '@/types/app' import { ModelModeType } from '@/types/app'
import { VIBE_APPLY_EVENT, VIBE_COMMAND_EVENT } from '../../constants' import { VIBE_APPLY_EVENT, VIBE_COMMAND_EVENT } from '../../constants'
import { useHooksStore } from '../../hooks-store'
import { useVibeFlowData } from '../../hooks/use-workflow-vibe'
import { useStore, useWorkflowStore } from '../../store' import { useStore, useWorkflowStore } from '../../store'
import WorkflowPreview from '../../workflow-preview' import WorkflowPreview from '../../workflow-preview'
@ -31,11 +29,9 @@ const VibePanel: FC = () => {
const showVibePanel = useStore(s => s.showVibePanel) const showVibePanel = useStore(s => s.showVibePanel)
const isVibeGenerating = useStore(s => s.isVibeGenerating) const isVibeGenerating = useStore(s => s.isVibeGenerating)
const vibePanelInstruction = useStore(s => s.vibePanelInstruction) const vibePanelInstruction = useStore(s => s.vibePanelInstruction)
const configsMap = useHooksStore(s => s.configsMap) const currentFlowGraph = useStore(s => s.currentVibeFlow)
const versions = useStore(s => s.vibeFlowVersions)
const { current: currentFlowGraph, versions, currentVersionIndex, setCurrentVersionIndex } = useVibeFlowData({ const currentVersionIndex = useStore(s => s.vibeFlowCurrentIndex)
storageKey: configsMap?.flowId || '',
})
const vibePanelPreviewNodes = currentFlowGraph?.nodes || [] const vibePanelPreviewNodes = currentFlowGraph?.nodes || []
const vibePanelPreviewEdges = currentFlowGraph?.edges || [] const vibePanelPreviewEdges = currentFlowGraph?.edges || []
@ -124,6 +120,11 @@ const VibePanel: FC = () => {
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') }) Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}, [workflowStore, t]) }, [workflowStore, t])
const handleVersionChange = useCallback((index: number) => {
const { setVibeFlowCurrentIndex } = workflowStore.getState()
setVibeFlowCurrentIndex(index)
}, [workflowStore])
if (!showVibePanel) if (!showVibePanel)
return null return null
@ -139,6 +140,7 @@ const VibePanel: FC = () => {
isShow={showVibePanel} isShow={showVibePanel}
onClose={handleClose} onClose={handleClose}
className="min-w-[1140px] !p-0" className="min-w-[1140px] !p-0"
wrapperClassName="z-[900]"
clickOutsideNotClose clickOutsideNotClose
> >
<div className="flex h-[680px] flex-wrap"> <div className="flex h-[680px] flex-wrap">
@ -193,7 +195,8 @@ const VibePanel: FC = () => {
<VersionSelector <VersionSelector
versionLen={versions.length} versionLen={versions.length}
value={currentVersionIndex} value={currentVersionIndex}
onChange={setCurrentVersionIndex} onChange={handleVersionChange}
contentClassName="z-[1200]"
/> />
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-2">
@ -216,6 +219,7 @@ const VibePanel: FC = () => {
</div> </div>
<div className="flex grow flex-col overflow-hidden pb-6"> <div className="flex grow flex-col overflow-hidden pb-6">
<WorkflowPreview <WorkflowPreview
key={currentVersionIndex}
nodes={vibePanelPreviewNodes} nodes={vibePanelPreviewNodes}
edges={vibePanelPreviewEdges} edges={vibePanelPreviewEdges}
viewport={{ x: 0, y: 0, zoom: 1 }} viewport={{ x: 0, y: 0, zoom: 1 }}

View File

@ -1,4 +1,10 @@
import type { StateCreator } from 'zustand' import type { StateCreator } from 'zustand'
import type { Edge, Node } from '../../types'
export type FlowGraph = {
nodes: Node[]
edges: Edge[]
}
export type VibeWorkflowSliceShape = { export type VibeWorkflowSliceShape = {
vibePanelMermaidCode: string vibePanelMermaidCode: string
@ -7,13 +13,66 @@ export type VibeWorkflowSliceShape = {
setIsVibeGenerating: (isVibeGenerating: boolean) => void setIsVibeGenerating: (isVibeGenerating: boolean) => void
vibePanelInstruction: string vibePanelInstruction: string
setVibePanelInstruction: (vibePanelInstruction: string) => void setVibePanelInstruction: (vibePanelInstruction: string) => void
vibeFlowVersions: FlowGraph[]
setVibeFlowVersions: (versions: FlowGraph[]) => void
vibeFlowCurrentIndex: number
setVibeFlowCurrentIndex: (index: number) => void
addVibeFlowVersion: (version: FlowGraph) => void
currentVibeFlow: FlowGraph | undefined
} }
export const createVibeWorkflowSlice: StateCreator<VibeWorkflowSliceShape> = set => ({ const getCurrentVibeFlow = (versions: FlowGraph[], currentIndex: number): FlowGraph | undefined => {
if (!versions || versions.length === 0)
return undefined
const index = currentIndex ?? 0
if (index < 0)
return undefined
return versions[index] || versions[versions.length - 1]
}
export const createVibeWorkflowSlice: StateCreator<VibeWorkflowSliceShape> = (set, get) => ({
vibePanelMermaidCode: '', vibePanelMermaidCode: '',
setVibePanelMermaidCode: vibePanelMermaidCode => set(() => ({ vibePanelMermaidCode })), setVibePanelMermaidCode: vibePanelMermaidCode => set(() => ({ vibePanelMermaidCode })),
isVibeGenerating: false, isVibeGenerating: false,
setIsVibeGenerating: isVibeGenerating => set(() => ({ isVibeGenerating })), setIsVibeGenerating: isVibeGenerating => set(() => ({ isVibeGenerating })),
vibePanelInstruction: '', vibePanelInstruction: '',
setVibePanelInstruction: vibePanelInstruction => set(() => ({ vibePanelInstruction })), setVibePanelInstruction: vibePanelInstruction => set(() => ({ vibePanelInstruction })),
vibeFlowVersions: [],
setVibeFlowVersions: versions => set((state) => {
const currentVibeFlow = getCurrentVibeFlow(versions, state.vibeFlowCurrentIndex)
return { vibeFlowVersions: versions, currentVibeFlow }
}),
vibeFlowCurrentIndex: 0,
setVibeFlowCurrentIndex: (index) => {
const state = get()
const versions = state.vibeFlowVersions || []
if (!versions || versions.length === 0) {
set({ vibeFlowCurrentIndex: 0, currentVibeFlow: undefined })
return
}
const normalizedIndex = Math.min(Math.max(index, 0), versions.length - 1)
const currentVibeFlow = getCurrentVibeFlow(versions, normalizedIndex)
set({ vibeFlowCurrentIndex: normalizedIndex, currentVibeFlow })
},
addVibeFlowVersion: (version) => {
// Prevent adding empty graphs
if (!version || !version.nodes || version.nodes.length === 0) {
set({ vibeFlowCurrentIndex: -1, currentVibeFlow: undefined })
return
}
set((state) => {
const newVersions = [...(state.vibeFlowVersions || []), version]
const newIndex = newVersions.length - 1
const currentVibeFlow = getCurrentVibeFlow(newVersions, newIndex)
return {
vibeFlowVersions: newVersions,
vibeFlowCurrentIndex: newIndex,
currentVibeFlow,
}
})
},
currentVibeFlow: undefined,
}) })