feat: implement Vibe panel for workflow with regeneration and acceptance features

This commit is contained in:
WTW0313 2025-12-25 22:19:57 +08:00 committed by crazywoola
parent b7be5c8c82
commit 0f69e2f6ab
7 changed files with 454 additions and 212 deletions

View File

@ -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,

View File

@ -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<Model | null>(null)
const isGeneratingRef = useRef(false)
const lastInstructionRef = useRef<string>('')
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<string, Node>()
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<string, unknown>),
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<string, Node>()
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<string, unknown>),
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<VibeCommandDetail>) => {
handleVibeCommand(event.detail?.dsl)
handleVibeCommand(event.detail?.dsl, false)
}
const regenerateHandler = () => {
handleRegenerate()
}
const acceptHandler = (event: CustomEvent<VibeCommandDetail>) => {
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
}

View File

@ -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)

View File

@ -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<PanelProps> = ({
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<PanelProps> = ({
)
return (
<div
ref={rightPanelRef}
tabIndex={-1}
className={cn('absolute bottom-1 right-0 top-14 z-10 flex outline-none')}
key={`${isRestoring}`}
>
{components?.left}
{!!selectedNode && <NodePanel {...selectedNode} />}
<>
<div
className="relative"
ref={otherPanelRef}
ref={rightPanelRef}
tabIndex={-1}
className={cn('absolute bottom-1 right-0 top-14 z-10 flex outline-none')}
key={`${isRestoring}`}
>
{
components?.right
}
{
showWorkflowVersionHistoryPanel && (
<VersionHistoryPanel {...versionHistoryPanelProps} />
)
}
{
showEnvPanel && (
<EnvPanel />
)
}
{components?.left}
{!!selectedNode && <NodePanel {...selectedNode} />}
<div
className="relative"
ref={otherPanelRef}
>
{
components?.right
}
{
showWorkflowVersionHistoryPanel && (
<VersionHistoryPanel {...versionHistoryPanelProps} />
)
}
{
showEnvPanel && (
<EnvPanel />
)
}
</div>
</div>
</div>
{showVibePanel && <VibePanel />}
</>
)
}

View File

@ -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 (
<div
className={cn(
'absolute bottom-0 right-0 top-0 z-20',
'flex flex-col',
'w-[600px] border-l border-divider-subtle',
'bg-components-panel-bg backdrop-blur-[10px]',
'rounded-xl shadow-xl',
)}
>
{/* Header */}
<div className="flex items-center justify-between border-b border-divider-subtle px-4 py-3">
<div className="text-sm font-semibold text-text-primary">
{t('workflow.vibe.panelTitle')}
</div>
<button
onClick={handleClose}
className="rounded-lg p-1 transition-colors hover:bg-state-base-hover"
>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</button>
</div>
{/* Content */}
<div className="flex-1 overflow-y-auto p-4">
{isVibeGenerating && !vibePanelMermaidCode
? (
<div className="flex h-full flex-col items-center justify-center gap-4">
<RiLoader2Line className="h-4 w-4 animate-spin text-text-tertiary" />
<div className="text-sm text-text-tertiary">
{t('workflow.vibe.generatingFlowchart')}
</div>
</div>
)
: vibePanelMermaidCode
? (
<div className="h-full">
<Flowchart
PrimitiveCode={vibePanelMermaidCode}
theme="light"
/>
</div>
)
: (
<div className="flex h-full flex-col items-center justify-center gap-2 text-sm text-text-tertiary">
<div>{t('workflow.vibe.noFlowchartYet')}</div>
</div>
)}
</div>
{/* Footer Actions */}
{vibePanelMermaidCode && !isVibeGenerating && (
<div className="flex items-center justify-end gap-2 border-t border-divider-subtle px-4 py-3">
<Button
variant="secondary"
size="medium"
onClick={handleRegenerate}
>
<RiRefreshLine className="mr-1 h-4 w-4" />
{t('workflow.vibe.regenerate')}
</Button>
<Button
variant="primary"
size="medium"
onClick={handleAccept}
>
<RiCheckLine className="mr-1 h-4 w-4" />
{t('workflow.vibe.accept')}
</Button>
</div>
)}
</div>
)
}
export default VibePanel

View File

@ -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<PanelSliceShape> = set => ({
@ -44,4 +50,10 @@ export const createPanelSlice: StateCreator<PanelSliceShape> = 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 })),
})

View File

@ -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',