From 4879795cb995ee4394d78ff6b8cde77c00af4062 Mon Sep 17 00:00:00 2001 From: WTW0313 Date: Sat, 27 Dec 2025 13:28:55 +0800 Subject: [PATCH] feat: update Vibe panel to use new event handling and versioning for flowcharts --- web/app/components/workflow/constants.ts | 3 +- .../workflow/hooks/use-workflow-vibe.tsx | 140 +++++++++++------- .../workflow/panel/vibe-panel/index.tsx | 107 +++++++------ .../workflow/store/workflow/index.ts | 4 + .../workflow/store/workflow/panel-slice.ts | 11 -- .../store/workflow/vibe-workflow-slice.ts | 19 +++ web/i18n/en-US/workflow.ts | 2 +- 7 files changed, 175 insertions(+), 111 deletions(-) create mode 100644 web/app/components/workflow/store/workflow/vibe-workflow-slice.ts diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 0a6bd74bff..b931facd5e 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -10,8 +10,7 @@ export const X_OFFSET = 60 export const NODE_WIDTH_X_OFFSET = NODE_WIDTH + X_OFFSET export const Y_OFFSET = 39 export const VIBE_COMMAND_EVENT = 'workflow-vibe-command' -export const VIBE_REGENERATE_EVENT = 'workflow-vibe-regenerate' -export const VIBE_ACCEPT_EVENT = 'workflow-vibe-accept' +export const VIBE_APPLY_EVENT = 'workflow-vibe-apply' export const START_INITIAL_POSITION = { x: 80, y: 282 } export const AUTO_LAYOUT_OFFSET = { x: -42, diff --git a/web/app/components/workflow/hooks/use-workflow-vibe.tsx b/web/app/components/workflow/hooks/use-workflow-vibe.tsx index 33c51953ef..57496cc1a6 100644 --- a/web/app/components/workflow/hooks/use-workflow-vibe.tsx +++ b/web/app/components/workflow/hooks/use-workflow-vibe.tsx @@ -4,6 +4,7 @@ import type { ToolDefaultValue } from '../block-selector/types' import type { Edge, Node, ToolWithProvider } from '../types' import type { Tool } from '@/app/components/tools/types' import type { Model } from '@/types/app' +import { useSessionStorageState } from 'ahooks' import { useCallback, useEffect, useMemo, useRef, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStoreApi } from 'reactflow' @@ -25,10 +26,10 @@ import { CUSTOM_EDGE, NODE_WIDTH, NODE_WIDTH_X_OFFSET, - VIBE_ACCEPT_EVENT, + VIBE_APPLY_EVENT, VIBE_COMMAND_EVENT, - VIBE_REGENERATE_EVENT, } from '../constants' +import { useHooksStore } from '../hooks-store' import { useWorkflowStore } from '../store' import { BlockEnum } from '../types' import { @@ -76,6 +77,11 @@ type ParseResult = { edges: ParsedEdge[] } +type FlowGraph = { + nodes: Node[] + edges: Edge[] +} + const NODE_DECLARATION = /^([A-Z][\w-]*)\s*\[(?:"([^"]+)"|([^\]]+))\]\s*$/i const EDGE_DECLARATION = /^(.+?)\s*-->\s*(?:\|([^|]+)\|\s*)?(.+)$/ @@ -276,10 +282,45 @@ const buildToolParams = (parameters?: Tool['parameters']) => { return params } +type UseVibeFlowDataParams = { + storageKey: string +} + +const keyPrefix = 'vibe-flow-' + +export const useVibeFlowData = ({ storageKey }: UseVibeFlowDataParams) => { + const [versions, setVersions] = useSessionStorageState(`${keyPrefix}${storageKey}-versions`, { + defaultValue: [], + }) + + const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState(`${keyPrefix}${storageKey}-version-index`, { + defaultValue: 0, + }) + + const current = versions?.[currentVersionIndex || 0] + + const addVersion = useCallback((version: FlowGraph) => { + setCurrentVersionIndex(() => versions?.length || 0) + setVersions((prev) => { + return [...prev!, version] + }) + }, [setVersions, setCurrentVersionIndex, versions?.length]) + + return { + versions, + addVersion, + currentVersionIndex, + setCurrentVersionIndex, + current, + } +} + export const useWorkflowVibe = () => { const { t } = useTranslation() const store = useStoreApi() const workflowStore = useWorkflowStore() + const configsMap = useHooksStore(s => s.configsMap) + const language = useGetLanguage() const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const { handleSyncWorkflowDraft } = useNodesSyncDraft() @@ -296,6 +337,10 @@ export const useWorkflowVibe = () => { const isGeneratingRef = useRef(false) const lastInstructionRef = useRef('') + const { addVersion, current: currentFlowGraph } = useVibeFlowData({ + storageKey: `${configsMap?.flowId}`, + }) + useEffect(() => { const storedModel = (() => { if (typeof window === 'undefined') @@ -427,48 +472,42 @@ export const useWorkflowVibe = () => { return map }, [nodesMetaDataMap]) - const applyFlowchartToWorkflow = useCallback(async (mermaidCode: string) => { - const { getNodes, setNodes, edges, setEdges } = store.getState() + const flowchartToWorkflowGraph = useCallback(async (mermaidCode: string): Promise => { + const { getNodes } = store.getState() const nodes = getNodes() - const { - setShowVibePanel, - } = workflowStore.getState() const parseResultToUse = parseMermaidFlowchart(mermaidCode, nodeTypeLookup, toolLookup) + const emptyGraph = { + nodes: [], + edges: [], + } if ('error' in parseResultToUse) { switch (parseResultToUse.error) { case 'missingNodeType': case 'missingNodeDefinition': Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - setShowVibePanel(false) - return + return emptyGraph case 'unknownNodeId': Toast.notify({ type: 'error', message: t('workflow.vibe.unknownNodeId', { id: parseResultToUse.detail }) }) - setShowVibePanel(false) - return + return emptyGraph case 'unknownNodeType': Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResultToUse.detail }) }) - setShowVibePanel(false) - return + return emptyGraph case 'unknownTool': Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResultToUse.detail }) }) - setShowVibePanel(false) - return + return emptyGraph case 'unsupportedEdgeLabel': Toast.notify({ type: 'error', message: t('workflow.vibe.unsupportedEdgeLabel', { label: parseResultToUse.detail }) }) - setShowVibePanel(false) - return + return emptyGraph default: Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - setShowVibePanel(false) - return + return emptyGraph } } if (!nodesMetaDataMap) { Toast.notify({ type: 'error', message: t('workflow.vibe.nodesUnavailable') }) - setShowVibePanel(false) - return + return emptyGraph } const existingStartNode = nodes.find(node => node.data.type === BlockEnum.Start) @@ -513,7 +552,7 @@ export const useWorkflowVibe = () => { if (!newNodes.length) { Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - return + return emptyGraph } const buildEdge = ( @@ -626,10 +665,20 @@ export const useWorkflowVibe = () => { }, } }) + return { + nodes: updatedNodes, + edges: newEdges, + } + }, [nodeTypeLookup, toolLookup]) - setNodes(updatedNodes) - setEdges([...edges, ...newEdges]) - saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNodes[0].id }) + const applyFlowchartToWorkflow = useCallback(() => { + const { setNodes, setEdges } = store.getState() + const vibePanelPreviewNodes = currentFlowGraph.nodes || [] + const vibePanelPreviewEdges = currentFlowGraph.edges || [] + + setNodes(vibePanelPreviewNodes) + setEdges(vibePanelPreviewEdges) + saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: vibePanelPreviewNodes[0].id }) handleSyncWorkflowDraft() workflowStore.setState(state => ({ @@ -744,8 +793,11 @@ export const useWorkflowVibe = () => { isVibeGenerating: false, })) + const workflowGraph = await flowchartToWorkflowGraph(mermaidCode) + addVersion(workflowGraph) + if (skipPanelPreview) - await applyFlowchartToWorkflow(mermaidCode) + applyFlowchartToWorkflow() } finally { isGeneratingRef.current = false @@ -764,47 +816,27 @@ export const useWorkflowVibe = () => { getLatestModelConfig, ]) - const handleRegenerate = useCallback(async () => { - if (!lastInstructionRef.current) { - Toast.notify({ type: 'error', message: t('workflow.vibe.missingInstruction') }) - return - } - - await handleVibeCommand(lastInstructionRef.current, false) - }, [handleVibeCommand, t]) - - const handleAccept = useCallback(async (vibePanelMermaidCode: string | undefined) => { - if (!vibePanelMermaidCode) { - Toast.notify({ type: 'error', message: t('workflow.vibe.noFlowchart') }) - return - } - - await applyFlowchartToWorkflow(vibePanelMermaidCode) - }, [applyFlowchartToWorkflow, t]) + const handleAccept = useCallback(() => { + applyFlowchartToWorkflow() + }, [applyFlowchartToWorkflow]) useEffect(() => { const handler = (event: CustomEvent) => { handleVibeCommand(event.detail?.dsl, false) } - const regenerateHandler = () => { - handleRegenerate() - } - - const acceptHandler = (event: CustomEvent) => { - handleAccept(event.detail?.dsl) + const acceptHandler = () => { + handleAccept() } document.addEventListener(VIBE_COMMAND_EVENT, handler as EventListener) - document.addEventListener(VIBE_REGENERATE_EVENT, regenerateHandler as EventListener) - document.addEventListener(VIBE_ACCEPT_EVENT, acceptHandler as EventListener) + document.addEventListener(VIBE_APPLY_EVENT, acceptHandler as EventListener) return () => { document.removeEventListener(VIBE_COMMAND_EVENT, handler as EventListener) - document.removeEventListener(VIBE_REGENERATE_EVENT, regenerateHandler as EventListener) - document.removeEventListener(VIBE_ACCEPT_EVENT, acceptHandler as EventListener) + document.removeEventListener(VIBE_APPLY_EVENT, acceptHandler as EventListener) } - }, [handleVibeCommand, handleRegenerate]) + }, [handleVibeCommand, handleAccept]) return null } diff --git a/web/app/components/workflow/panel/vibe-panel/index.tsx b/web/app/components/workflow/panel/vibe-panel/index.tsx index 684cf4d96e..119ceaa012 100644 --- a/web/app/components/workflow/panel/vibe-panel/index.tsx +++ b/web/app/components/workflow/panel/vibe-panel/index.tsx @@ -3,33 +3,42 @@ import type { FC } from 'react' import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations' import type { CompletionParams, Model } from '@/types/app' -import { RiCheckLine, RiRefreshLine } from '@remixicon/react' +import { RiClipboardLine } from '@remixicon/react' +import copy from 'copy-to-clipboard' import { useCallback, useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' import ResPlaceholder from '@/app/components/app/configuration/config/automatic/res-placeholder' +import VersionSelector from '@/app/components/app/configuration/config/automatic/version-selector' import Button from '@/app/components/base/button' import { Generator } from '@/app/components/base/icons/src/vender/other' import Loading from '@/app/components/base/loading' -import Flowchart from '@/app/components/base/mermaid' import Modal from '@/app/components/base/modal' import Textarea from '@/app/components/base/textarea' +import Toast from '@/app/components/base/toast' import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks' import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal' import { ModelModeType } from '@/types/app' -import { VIBE_ACCEPT_EVENT, VIBE_COMMAND_EVENT, VIBE_REGENERATE_EVENT } from '../../constants' -import { useStore } from '../../store' +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 WorkflowPreview from '../../workflow-preview' const VibePanel: FC = () => { const { t } = useTranslation() + const workflowStore = useWorkflowStore() const showVibePanel = useStore(s => s.showVibePanel) - const setShowVibePanel = useStore(s => s.setShowVibePanel) - const vibePanelMermaidCode = useStore(s => s.vibePanelMermaidCode) - const setVibePanelMermaidCode = useStore(s => s.setVibePanelMermaidCode) const isVibeGenerating = useStore(s => s.isVibeGenerating) - const setIsVibeGenerating = useStore(s => s.setIsVibeGenerating) const vibePanelInstruction = useStore(s => s.vibePanelInstruction) - const setVibePanelInstruction = useStore(s => s.setVibePanelInstruction) + const configsMap = useHooksStore(s => s.configsMap) + + const { current: currentFlowGraph, versions, currentVersionIndex, setCurrentVersionIndex } = useVibeFlowData({ + storageKey: `${configsMap?.flowId}`, + }) + + const vibePanelPreviewNodes = currentFlowGraph?.nodes || [] + const vibePanelPreviewEdges = currentFlowGraph?.edges || [] const localModel = localStorage.getItem('auto-gen-model') ? JSON.parse(localStorage.getItem('auto-gen-model') as string) as Model @@ -80,11 +89,21 @@ const VibePanel: FC = () => { localStorage.setItem('auto-gen-model', JSON.stringify(newModel)) }, [model]) + const handleInstructionChange = useCallback((e: React.ChangeEvent) => { + workflowStore.setState(state => ({ + ...state, + vibePanelInstruction: e.target.value, + })) + }, [workflowStore]) + const handleClose = useCallback(() => { - setShowVibePanel(false) - setVibePanelMermaidCode('') - setIsVibeGenerating(false) - }, [setShowVibePanel, setVibePanelMermaidCode, setIsVibeGenerating]) + workflowStore.setState(state => ({ + ...state, + showVibePanel: false, + vibePanelMermaidCode: '', + isVibeGenerating: false, + })) + }, [workflowStore]) const handleGenerate = useCallback(() => { const event = new CustomEvent(VIBE_COMMAND_EVENT, { @@ -94,20 +113,16 @@ const VibePanel: FC = () => { }, [vibePanelInstruction]) const handleAccept = useCallback(() => { - if (vibePanelMermaidCode) { - const event = new CustomEvent(VIBE_ACCEPT_EVENT, { - detail: { dsl: vibePanelMermaidCode }, - }) - document.dispatchEvent(event) - handleClose() - } - }, [vibePanelMermaidCode, handleClose]) - - const handleRegenerate = useCallback(() => { - setIsVibeGenerating(true) - const event = new CustomEvent(VIBE_REGENERATE_EVENT) + const event = new CustomEvent(VIBE_APPLY_EVENT) document.dispatchEvent(event) - }, [setIsVibeGenerating]) + handleClose() + }, [handleClose]) + + const handleCopyMermaid = useCallback(() => { + const { vibePanelMermaidCode } = workflowStore.getState() + copy(vibePanelMermaidCode) + Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') }) + }, [workflowStore, t]) if (!showVibePanel) return null @@ -150,7 +165,7 @@ const VibePanel: FC = () => { className="min-h-[240px] resize-none rounded-[10px] px-4 pt-3" placeholder={t('workflow.vibe.missingInstruction')} value={vibePanelInstruction} - onChange={e => setVibePanelInstruction(e.target.value)} + onChange={handleInstructionChange} /> @@ -163,48 +178,54 @@ const VibePanel: FC = () => { disabled={isVibeGenerating} > - {t('appDebug.generate.generate')} + {t('appDebug.generate.generate')} - {!isVibeGenerating && vibePanelMermaidCode && ( + {!isVibeGenerating && vibePanelPreviewNodes.length > 0 && (
-
{t('workflow.vibe.panelTitle')}
+
+
{t('workflow.vibe.panelTitle')}
+ +
-
-
- -
+
+
)} {isVibeGenerating && renderLoading} - {!isVibeGenerating && !vibePanelMermaidCode && } + {!isVibeGenerating && vibePanelPreviewNodes.length === 0 && }
) diff --git a/web/app/components/workflow/store/workflow/index.ts b/web/app/components/workflow/store/workflow/index.ts index c2c0c00201..e9416e6e1b 100644 --- a/web/app/components/workflow/store/workflow/index.ts +++ b/web/app/components/workflow/store/workflow/index.ts @@ -12,6 +12,7 @@ import type { NodeSliceShape } from './node-slice' import type { PanelSliceShape } from './panel-slice' import type { ToolSliceShape } from './tool-slice' import type { VersionSliceShape } from './version-slice' +import type { VibeWorkflowSliceShape } from './vibe-workflow-slice' import type { WorkflowDraftSliceShape } from './workflow-draft-slice' import type { WorkflowSliceShape } from './workflow-slice' import type { RagPipelineSliceShape } from '@/app/components/rag-pipeline/store' @@ -34,6 +35,7 @@ import { createNodeSlice } from './node-slice' import { createPanelSlice } from './panel-slice' import { createToolSlice } from './tool-slice' import { createVersionSlice } from './version-slice' +import { createVibeWorkflowSlice } from './vibe-workflow-slice' import { createWorkflowDraftSlice } from './workflow-draft-slice' import { createWorkflowSlice } from './workflow-slice' @@ -56,6 +58,7 @@ export type Shape & InspectVarsSliceShape & LayoutSliceShape & SliceFromInjection + & VibeWorkflowSliceShape export type InjectWorkflowStoreSliceFn = StateCreator @@ -80,6 +83,7 @@ export const createWorkflowStore = (params: CreateWorkflowStoreParams) => { ...createWorkflowSlice(...args), ...createInspectVarsSlice(...args), ...createLayoutSlice(...args), + ...createVibeWorkflowSlice(...args), ...(injectWorkflowStoreSliceFn?.(...args) || {} as SliceFromInjection), })) } diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts index c695854dcb..afd20be898 100644 --- a/web/app/components/workflow/store/workflow/panel-slice.ts +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -26,12 +26,6 @@ export type PanelSliceShape = { setInitShowLastRunTab: (initShowLastRunTab: boolean) => void showVibePanel: boolean setShowVibePanel: (showVibePanel: boolean) => void - vibePanelMermaidCode: string - setVibePanelMermaidCode: (vibePanelMermaidCode: string) => void - isVibeGenerating: boolean - setIsVibeGenerating: (isVibeGenerating: boolean) => void - vibePanelInstruction: string - setVibePanelInstruction: (vibePanelInstruction: string) => void } export const createPanelSlice: StateCreator = set => ({ @@ -55,9 +49,4 @@ export const createPanelSlice: StateCreator = set => ({ showVibePanel: false, setShowVibePanel: showVibePanel => set(() => ({ showVibePanel })), vibePanelMermaidCode: '', - setVibePanelMermaidCode: vibePanelMermaidCode => set(() => ({ vibePanelMermaidCode })), - isVibeGenerating: false, - setIsVibeGenerating: isVibeGenerating => set(() => ({ isVibeGenerating })), - vibePanelInstruction: '', - setVibePanelInstruction: vibePanelInstruction => set(() => ({ vibePanelInstruction })), }) diff --git a/web/app/components/workflow/store/workflow/vibe-workflow-slice.ts b/web/app/components/workflow/store/workflow/vibe-workflow-slice.ts new file mode 100644 index 0000000000..07bd6c4c5b --- /dev/null +++ b/web/app/components/workflow/store/workflow/vibe-workflow-slice.ts @@ -0,0 +1,19 @@ +import type { StateCreator } from 'zustand' + +export type VibeWorkflowSliceShape = { + vibePanelMermaidCode: string + setVibePanelMermaidCode: (vibePanelMermaidCode: string) => void + isVibeGenerating: boolean + setIsVibeGenerating: (isVibeGenerating: boolean) => void + vibePanelInstruction: string + setVibePanelInstruction: (vibePanelInstruction: string) => void +} + +export const createVibeWorkflowSlice: StateCreator = set => ({ + vibePanelMermaidCode: '', + setVibePanelMermaidCode: vibePanelMermaidCode => set(() => ({ vibePanelMermaidCode })), + isVibeGenerating: false, + setIsVibeGenerating: isVibeGenerating => set(() => ({ isVibeGenerating })), + vibePanelInstruction: '', + setVibePanelInstruction: vibePanelInstruction => set(() => ({ vibePanelInstruction })), +}) diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index bc6b700f54..9d00be30c7 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -138,7 +138,7 @@ const translation = { generatingFlowchart: 'Generating flowchart preview...', noFlowchartYet: 'No flowchart preview available', regenerate: 'Regenerate', - accept: 'Accept', + apply: 'Apply', noFlowchart: 'No flowchart provided', }, publishLimit: {