From 0f69e2f6ab7aa895e79132ce26cfa243b5b7b16e Mon Sep 17 00:00:00 2001 From: WTW0313 Date: Thu, 25 Dec 2025 22:19:57 +0800 Subject: [PATCH] feat: implement Vibe panel for workflow with regeneration and acceptance features --- web/app/components/workflow/constants.ts | 2 + .../workflow/hooks/use-workflow-vibe.tsx | 469 +++++++++++------- .../components/workflow/hooks/use-workflow.ts | 2 + web/app/components/workflow/panel/index.tsx | 53 +- .../workflow/panel/vibe-panel/index.tsx | 122 +++++ .../workflow/store/workflow/panel-slice.ts | 12 + web/i18n/en-US/workflow.ts | 6 + 7 files changed, 454 insertions(+), 212 deletions(-) create mode 100644 web/app/components/workflow/panel/vibe-panel/index.tsx diff --git a/web/app/components/workflow/constants.ts b/web/app/components/workflow/constants.ts index 8fc179a4f2..0a6bd74bff 100644 --- a/web/app/components/workflow/constants.ts +++ b/web/app/components/workflow/constants.ts @@ -10,6 +10,8 @@ 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 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 d1f4637389..705147cac1 100644 --- a/web/app/components/workflow/hooks/use-workflow-vibe.tsx +++ b/web/app/components/workflow/hooks/use-workflow-vibe.tsx @@ -25,8 +25,11 @@ import { CUSTOM_EDGE, NODE_WIDTH, NODE_WIDTH_X_OFFSET, + VIBE_ACCEPT_EVENT, VIBE_COMMAND_EVENT, + VIBE_REGENERATE_EVENT, } from '../constants' +import { useWorkflowStore } from '../store' import { BlockEnum } from '../types' import { generateNewNode, @@ -276,6 +279,7 @@ const buildToolParams = (parameters?: Tool['parameters']) => { export const useWorkflowVibe = () => { const { t } = useTranslation() const store = useStoreApi() + const workflowStore = useWorkflowStore() const language = useGetLanguage() const { nodesMap: nodesMetaDataMap } = useNodesMetaData() const { handleSyncWorkflowDraft } = useNodesSyncDraft() @@ -290,6 +294,7 @@ export const useWorkflowVibe = () => { const [modelConfig, setModelConfig] = useState(null) const isGeneratingRef = useRef(false) + const lastInstructionRef = useRef('') useEffect(() => { const storedModel = (() => { @@ -408,7 +413,227 @@ export const useWorkflowVibe = () => { return map }, [nodesMetaDataMap]) - const handleVibeCommand = useCallback(async (dsl?: string) => { + const applyFlowchartToWorkflow = useCallback(async (mermaidCode: string) => { + const { getNodes, setNodes, edges, setEdges } = store.getState() + const nodes = getNodes() + const { + setShowVibePanel, + } = workflowStore.getState() + + const parseResultToUse = parseMermaidFlowchart(mermaidCode, nodeTypeLookup, toolLookup) + if ('error' in parseResultToUse) { + switch (parseResultToUse.error) { + case 'missingNodeType': + case 'missingNodeDefinition': + Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) + setShowVibePanel(false) + return + case 'unknownNodeId': + Toast.notify({ type: 'error', message: t('workflow.vibe.unknownNodeId', { id: parseResultToUse.detail }) }) + setShowVibePanel(false) + return + case 'unknownNodeType': + Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResultToUse.detail }) }) + setShowVibePanel(false) + return + case 'unknownTool': + Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResultToUse.detail }) }) + setShowVibePanel(false) + return + case 'unsupportedEdgeLabel': + Toast.notify({ type: 'error', message: t('workflow.vibe.unsupportedEdgeLabel', { label: parseResultToUse.detail }) }) + setShowVibePanel(false) + return + default: + Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) + setShowVibePanel(false) + return + } + } + + if (!nodesMetaDataMap) { + Toast.notify({ type: 'error', message: t('workflow.vibe.nodesUnavailable') }) + setShowVibePanel(false) + return + } + + const existingStartNode = nodes.find(node => node.data.type === BlockEnum.Start) + const newNodes: Node[] = [] + const nodeIdMap = new Map() + + parseResultToUse.nodes.forEach((nodeSpec) => { + if (nodeSpec.type === BlockEnum.Start && existingStartNode) { + nodeIdMap.set(nodeSpec.id, existingStartNode) + return + } + + const nodeDefault = nodesMetaDataMap![nodeSpec.type] + if (!nodeDefault) + return + + const defaultValue = nodeDefault.defaultValue || {} + const title = nodeSpec.title?.trim() || nodeDefault.metaData.title || defaultValue.title || nodeSpec.type + + const toolDefaultValue = nodeSpec.toolKey ? toolLookup.get(nodeSpec.toolKey) : undefined + const desc = (toolDefaultValue?.tool_description || (defaultValue as { desc?: string }).desc || '') as string + + const data = { + ...(defaultValue as Record), + title, + desc, + type: nodeSpec.type, + selected: false, + ...(toolDefaultValue || {}), + } + + const newNode = generateNewNode({ + id: uuid4(), + type: getNodeCustomTypeByNodeDataType(nodeSpec.type), + data, + position: { x: 0, y: 0 }, + }).newNode + + newNodes.push(newNode) + nodeIdMap.set(nodeSpec.id, newNode) + }) + + if (!newNodes.length) { + Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) + return + } + + const buildEdge = ( + source: Node, + target: Node, + sourceHandle = 'source', + targetHandle = 'target', + ): Edge => ({ + id: `${source.id}-${sourceHandle}-${target.id}-${targetHandle}`, + type: CUSTOM_EDGE, + source: source.id, + sourceHandle, + target: target.id, + targetHandle, + data: { + sourceType: source.data.type, + targetType: target.data.type, + isInIteration: false, + isInLoop: false, + _connectedNodeIsSelected: false, + }, + zIndex: 0, + }) + + const newEdges: Edge[] = [] + for (const edgeSpec of parseResultToUse.edges) { + const sourceNode = nodeIdMap.get(edgeSpec.sourceId) + const targetNode = nodeIdMap.get(edgeSpec.targetId) + if (!sourceNode || !targetNode) + continue + + let sourceHandle = 'source' + if (sourceNode.data.type === BlockEnum.IfElse) { + const branchLabel = normalizeBranchLabel(edgeSpec.label) + if (branchLabel === 'true') { + sourceHandle = (sourceNode.data as { cases?: { case_id: string }[] })?.cases?.[0]?.case_id || 'true' + } + if (branchLabel === 'false') { + sourceHandle = 'false' + } + } + + newEdges.push(buildEdge(sourceNode, targetNode, sourceHandle)) + } + + const bounds = nodes.reduce( + (acc, node) => { + const width = node.width ?? NODE_WIDTH + acc.maxX = Math.max(acc.maxX, node.position.x + width) + acc.minY = Math.min(acc.minY, node.position.y) + return acc + }, + { maxX: 0, minY: 0 }, + ) + + const baseX = nodes.length ? bounds.maxX + NODE_WIDTH_X_OFFSET : 0 + const baseY = Number.isFinite(bounds.minY) ? bounds.minY : 0 + const branchOffset = Math.max(120, NODE_WIDTH_X_OFFSET / 2) + + const layoutNodeIds = new Set(newNodes.map(node => node.id)) + const layoutEdges = newEdges.filter(edge => + layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target), + ) + + try { + const layout = await getLayoutByDagre(newNodes, layoutEdges) + const layoutedNodes = newNodes.map((node) => { + const info = layout.nodes.get(node.id) + if (!info) + return node + return { + ...node, + position: { + x: baseX + info.x, + y: baseY + info.y, + }, + } + }) + newNodes.splice(0, newNodes.length, ...layoutedNodes) + } + catch { + newNodes.forEach((node, index) => { + const row = Math.floor(index / 4) + const col = index % 4 + node.position = { + x: baseX + col * NODE_WIDTH_X_OFFSET, + y: baseY + row * branchOffset, + } + }) + } + + const allNodes = [...nodes, ...newNodes] + const nodesConnectedMap = getNodesConnectedSourceOrTargetHandleIdsMap( + newEdges.map(edge => ({ type: 'add', edge })), + allNodes, + ) + + const updatedNodes = allNodes.map((node) => { + const connected = nodesConnectedMap[node.id] + if (!connected) + return node + + return { + ...node, + data: { + ...node.data, + ...connected, + _connectedSourceHandleIds: dedupeHandles(connected._connectedSourceHandleIds), + _connectedTargetHandleIds: dedupeHandles(connected._connectedTargetHandleIds), + }, + } + }) + + setNodes(updatedNodes) + setEdges([...edges, ...newEdges]) + saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNodes[0].id }) + handleSyncWorkflowDraft() + + workflowStore.setState(state => ({ + ...state, + showVibePanel: false, + vibePanelMermaidCode: '', + })) + }, [ + handleSyncWorkflowDraft, + nodeTypeLookup, + nodesMetaDataMap, + saveStateToHistory, + store, + t, + toolLookup, + ]) + + const handleVibeCommand = useCallback(async (dsl?: string, skipPanelPreview = false) => { if (getNodesReadOnly()) { Toast.notify({ type: 'error', message: t('workflow.vibe.readOnly') }) return @@ -434,9 +659,22 @@ export const useWorkflowVibe = () => { return isGeneratingRef.current = true + if (!isMermaidFlowchart(trimmed)) + lastInstructionRef.current = trimmed + + workflowStore.setState(state => ({ + ...state, + showVibePanel: true, + isVibeGenerating: true, + vibePanelMermaidCode: '', + })) + try { - const { getNodes, setNodes, edges, setEdges } = store.getState() + const { getNodes } = store.getState() const nodes = getNodes() + const { + setIsVibeGenerating, + } = workflowStore.getState() const existingNodesPayload = nodes.map(node => ({ id: node.id, @@ -471,202 +709,27 @@ export const useWorkflowVibe = () => { if (error) { Toast.notify({ type: 'error', message: error }) + setIsVibeGenerating(false) return } if (!flowchart) { Toast.notify({ type: 'error', message: t('workflow.vibe.missingFlowchart') }) + setIsVibeGenerating(false) return } mermaidCode = flowchart } - const parseResult = parseMermaidFlowchart(mermaidCode, nodeTypeLookup, toolLookup) - if ('error' in parseResult) { - switch (parseResult.error) { - case 'missingNodeType': - case 'missingNodeDefinition': - Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - return - case 'unknownNodeId': - Toast.notify({ type: 'error', message: t('workflow.vibe.unknownNodeId', { id: parseResult.detail }) }) - return - case 'unknownNodeType': - Toast.notify({ type: 'error', message: t('workflow.vibe.nodeTypeUnavailable', { type: parseResult.detail }) }) - return - case 'unknownTool': - Toast.notify({ type: 'error', message: t('workflow.vibe.toolUnavailable', { tool: parseResult.detail }) }) - return - case 'unsupportedEdgeLabel': - Toast.notify({ type: 'error', message: t('workflow.vibe.unsupportedEdgeLabel', { label: parseResult.detail }) }) - return - default: - Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - return - } - } + workflowStore.setState(state => ({ + ...state, + vibePanelMermaidCode: mermaidCode, + isVibeGenerating: false, + })) - const existingStartNode = nodes.find(node => node.data.type === BlockEnum.Start) - const newNodes: Node[] = [] - const nodeIdMap = new Map() - - parseResult.nodes.forEach((nodeSpec) => { - if (nodeSpec.type === BlockEnum.Start && existingStartNode) { - nodeIdMap.set(nodeSpec.id, existingStartNode) - return - } - - const nodeDefault = nodesMetaDataMap[nodeSpec.type] - if (!nodeDefault) - return - - const defaultValue = nodeDefault.defaultValue || {} - const title = nodeSpec.title?.trim() || nodeDefault.metaData.title || defaultValue.title || nodeSpec.type - - const toolDefaultValue = nodeSpec.toolKey ? toolLookup.get(nodeSpec.toolKey) : undefined - const desc = (toolDefaultValue?.tool_description || (defaultValue as { desc?: string }).desc || '') as string - - const data = { - ...(defaultValue as Record), - title, - desc, - type: nodeSpec.type, - selected: false, - ...(toolDefaultValue || {}), - } - - const newNode = generateNewNode({ - id: uuid4(), - type: getNodeCustomTypeByNodeDataType(nodeSpec.type), - data, - position: { x: 0, y: 0 }, - }).newNode - - newNodes.push(newNode) - nodeIdMap.set(nodeSpec.id, newNode) - }) - - if (!newNodes.length) { - Toast.notify({ type: 'error', message: t('workflow.vibe.invalidFlowchart') }) - return - } - - const buildEdge = ( - source: Node, - target: Node, - sourceHandle = 'source', - targetHandle = 'target', - ): Edge => ({ - id: `${source.id}-${sourceHandle}-${target.id}-${targetHandle}`, - type: CUSTOM_EDGE, - source: source.id, - sourceHandle, - target: target.id, - targetHandle, - data: { - sourceType: source.data.type, - targetType: target.data.type, - isInIteration: false, - isInLoop: false, - _connectedNodeIsSelected: false, - }, - zIndex: 0, - }) - - const newEdges: Edge[] = [] - for (const edgeSpec of parseResult.edges) { - const sourceNode = nodeIdMap.get(edgeSpec.sourceId) - const targetNode = nodeIdMap.get(edgeSpec.targetId) - if (!sourceNode || !targetNode) - continue - - let sourceHandle = 'source' - if (sourceNode.data.type === BlockEnum.IfElse) { - const branchLabel = normalizeBranchLabel(edgeSpec.label) - if (branchLabel === 'true') { - sourceHandle = (sourceNode.data as { cases?: { case_id: string }[] })?.cases?.[0]?.case_id || 'true' - } - if (branchLabel === 'false') { - sourceHandle = 'false' - } - } - - newEdges.push(buildEdge(sourceNode, targetNode, sourceHandle)) - } - - const bounds = nodes.reduce( - (acc, node) => { - const width = node.width ?? NODE_WIDTH - acc.maxX = Math.max(acc.maxX, node.position.x + width) - acc.minY = Math.min(acc.minY, node.position.y) - return acc - }, - { maxX: 0, minY: 0 }, - ) - - const baseX = nodes.length ? bounds.maxX + NODE_WIDTH_X_OFFSET : 0 - const baseY = Number.isFinite(bounds.minY) ? bounds.minY : 0 - const branchOffset = Math.max(120, NODE_WIDTH_X_OFFSET / 2) - - const layoutNodeIds = new Set(newNodes.map(node => node.id)) - const layoutEdges = newEdges.filter(edge => - layoutNodeIds.has(edge.source) && layoutNodeIds.has(edge.target), - ) - - try { - const layout = await getLayoutByDagre(newNodes, layoutEdges) - const layoutedNodes = newNodes.map((node) => { - const info = layout.nodes.get(node.id) - if (!info) - return node - return { - ...node, - position: { - x: baseX + info.x, - y: baseY + info.y, - }, - } - }) - newNodes.splice(0, newNodes.length, ...layoutedNodes) - } - catch { - newNodes.forEach((node, index) => { - const row = Math.floor(index / 4) - const col = index % 4 - node.position = { - x: baseX + col * NODE_WIDTH_X_OFFSET, - y: baseY + row * branchOffset, - } - }) - } - - const allNodes = [...nodes, ...newNodes] - const nodesConnectedMap = getNodesConnectedSourceOrTargetHandleIdsMap( - newEdges.map(edge => ({ type: 'add', edge })), - allNodes, - ) - - const updatedNodes = allNodes.map((node) => { - const connected = nodesConnectedMap[node.id] - if (!connected) - return node - - return { - ...node, - data: { - ...node.data, - ...connected, - _connectedSourceHandleIds: dedupeHandles(connected._connectedSourceHandleIds), - _connectedTargetHandleIds: dedupeHandles(connected._connectedTargetHandleIds), - }, - } - }) - - setNodes(updatedNodes) - setEdges([...edges, ...newEdges]) - saveStateToHistory(WorkflowHistoryEvent.NodeAdd, { nodeId: newNodes[0].id }) - handleSyncWorkflowDraft() + if (skipPanelPreview) + await applyFlowchartToWorkflow(mermaidCode) } finally { isGeneratingRef.current = false @@ -685,17 +748,47 @@ export const useWorkflowVibe = () => { toolOptions, ]) + 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]) + useEffect(() => { const handler = (event: CustomEvent) => { - handleVibeCommand(event.detail?.dsl) + handleVibeCommand(event.detail?.dsl, false) + } + + const regenerateHandler = () => { + handleRegenerate() + } + + const acceptHandler = (event: CustomEvent) => { + handleAccept(event.detail?.dsl) } document.addEventListener(VIBE_COMMAND_EVENT, handler as EventListener) + document.addEventListener(VIBE_REGENERATE_EVENT, regenerateHandler as EventListener) + document.addEventListener(VIBE_ACCEPT_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) } - }, [handleVibeCommand]) + }, [handleVibeCommand, handleRegenerate]) return null } diff --git a/web/app/components/workflow/hooks/use-workflow.ts b/web/app/components/workflow/hooks/use-workflow.ts index 990c8c950d..c3e1df5961 100644 --- a/web/app/components/workflow/hooks/use-workflow.ts +++ b/web/app/components/workflow/hooks/use-workflow.ts @@ -471,12 +471,14 @@ export const useNodesReadOnly = () => { const workflowRunningData = useStore(s => s.workflowRunningData) const historyWorkflowData = useStore(s => s.historyWorkflowData) const isRestoring = useStore(s => s.isRestoring) + // const showVibePanel = useStore(s => s.showVibePanel) const getNodesReadOnly = useCallback((): boolean => { const { workflowRunningData, historyWorkflowData, isRestoring, + // showVibePanel, } = workflowStore.getState() return !!(workflowRunningData?.result.status === WorkflowRunningStatus.Running || historyWorkflowData || isRestoring) diff --git a/web/app/components/workflow/panel/index.tsx b/web/app/components/workflow/panel/index.tsx index 88ada8b11e..8b7ebfda63 100644 --- a/web/app/components/workflow/panel/index.tsx +++ b/web/app/components/workflow/panel/index.tsx @@ -8,6 +8,7 @@ import { cn } from '@/utils/classnames' import { Panel as NodePanel } from '../nodes' import { useStore } from '../store' import EnvPanel from './env-panel' +import VibePanel from './vibe-panel' const VersionHistoryPanel = dynamic(() => import('@/app/components/workflow/panel/version-history-panel'), { ssr: false, @@ -85,6 +86,7 @@ const Panel: FC = ({ const showEnvPanel = useStore(s => s.showEnvPanel) const isRestoring = useStore(s => s.isRestoring) const showWorkflowVersionHistoryPanel = useStore(s => s.showWorkflowVersionHistoryPanel) + const showVibePanel = useStore(s => s.showVibePanel) // widths used for adaptive layout const workflowCanvasWidth = useStore(s => s.workflowCanvasWidth) @@ -124,33 +126,36 @@ const Panel: FC = ({ ) return ( -
- {components?.left} - {!!selectedNode && } + <>
- { - components?.right - } - { - showWorkflowVersionHistoryPanel && ( - - ) - } - { - showEnvPanel && ( - - ) - } + {components?.left} + {!!selectedNode && } +
+ { + components?.right + } + { + showWorkflowVersionHistoryPanel && ( + + ) + } + { + showEnvPanel && ( + + ) + } +
-
+ {showVibePanel && } + ) } diff --git a/web/app/components/workflow/panel/vibe-panel/index.tsx b/web/app/components/workflow/panel/vibe-panel/index.tsx new file mode 100644 index 0000000000..af5b947bdc --- /dev/null +++ b/web/app/components/workflow/panel/vibe-panel/index.tsx @@ -0,0 +1,122 @@ +'use client' + +import type { FC } from 'react' +import { RiCheckLine, RiCloseLine, RiLoader2Line, RiRefreshLine } from '@remixicon/react' +import { useCallback } from 'react' +import { useTranslation } from 'react-i18next' +import Button from '@/app/components/base/button' +import Flowchart from '@/app/components/base/mermaid' +import { cn } from '@/utils/classnames' +import { VIBE_ACCEPT_EVENT, VIBE_REGENERATE_EVENT } from '../../constants' +import { useStore } from '../../store' + +const VibePanel: FC = () => { + const { t } = useTranslation() + 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 handleClose = useCallback(() => { + setShowVibePanel(false) + setVibePanelMermaidCode('') + setIsVibeGenerating(false) + }, [setShowVibePanel, setVibePanelMermaidCode, setIsVibeGenerating]) + + 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) + document.dispatchEvent(event) + }, [setIsVibeGenerating]) + + if (!showVibePanel) + return null + + return ( +
+ {/* Header */} +
+
+ {t('workflow.vibe.panelTitle')} +
+ +
+ + {/* Content */} +
+ {isVibeGenerating && !vibePanelMermaidCode + ? ( +
+ +
+ {t('workflow.vibe.generatingFlowchart')} +
+
+ ) + : vibePanelMermaidCode + ? ( +
+ +
+ ) + : ( +
+
{t('workflow.vibe.noFlowchartYet')}
+
+ )} +
+ + {/* Footer Actions */} + {vibePanelMermaidCode && !isVibeGenerating && ( +
+ + +
+ )} +
+ ) +} + +export default VibePanel diff --git a/web/app/components/workflow/store/workflow/panel-slice.ts b/web/app/components/workflow/store/workflow/panel-slice.ts index 4848beeac5..de712d8df4 100644 --- a/web/app/components/workflow/store/workflow/panel-slice.ts +++ b/web/app/components/workflow/store/workflow/panel-slice.ts @@ -24,6 +24,12 @@ export type PanelSliceShape = { setShowVariableInspectPanel: (showVariableInspectPanel: boolean) => void initShowLastRunTab: boolean setInitShowLastRunTab: (initShowLastRunTab: boolean) => void + showVibePanel: boolean + setShowVibePanel: (showVibePanel: boolean) => void + vibePanelMermaidCode: string + setVibePanelMermaidCode: (vibePanelMermaidCode: string) => void + isVibeGenerating: boolean + setIsVibeGenerating: (isVibeGenerating: boolean) => void } export const createPanelSlice: StateCreator = set => ({ @@ -44,4 +50,10 @@ export const createPanelSlice: StateCreator = set => ({ setShowVariableInspectPanel: showVariableInspectPanel => set(() => ({ showVariableInspectPanel })), initShowLastRunTab: false, setInitShowLastRunTab: initShowLastRunTab => set(() => ({ initShowLastRunTab })), + showVibePanel: false, + setShowVibePanel: showVibePanel => set(() => ({ showVibePanel })), + vibePanelMermaidCode: '', + setVibePanelMermaidCode: vibePanelMermaidCode => set(() => ({ vibePanelMermaidCode })), + isVibeGenerating: false, + setIsVibeGenerating: isVibeGenerating => set(() => ({ isVibeGenerating })), }) diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 022317d9b5..bc6b700f54 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -134,6 +134,12 @@ const translation = { toolUnavailable: 'Tool "{{tool}}" is not available in this workspace.', unknownNodeId: 'Node "{{id}}" is used before it is defined.', unsupportedEdgeLabel: 'Unsupported edge label "{{label}}". Only true/false are allowed for if/else.', + panelTitle: 'Workflow Preview', + generatingFlowchart: 'Generating flowchart preview...', + noFlowchartYet: 'No flowchart preview available', + regenerate: 'Regenerate', + accept: 'Accept', + noFlowchart: 'No flowchart provided', }, publishLimit: { startNodeTitlePrefix: 'Upgrade to',