Fix/prod ref (#59) (#65)

* fix(stores): moved subblock value to a separate store; execution currently doesn't work

* fix(execution): resolved merging stores

* fix(stores): deletion crash fix

* fix(stores): dropdown re-render
This commit is contained in:
Emir Karabeg
2025-02-18 18:27:35 -08:00
committed by GitHub
parent 0edbc7b78f
commit 756d748bc4
10 changed files with 263 additions and 97 deletions

View File

@@ -16,7 +16,7 @@ interface DropdownProps {
}
export function Dropdown({ options, defaultValue, blockId, subBlockId }: DropdownProps) {
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
const [value, setValue] = useSubBlockValue(blockId, subBlockId, true)
// Set the value to the first option if it's not set
useEffect(() => {

View File

@@ -1,24 +1,36 @@
import { useCallback } from 'react'
import { useWorkflowStore } from '@/stores/workflow/store'
import { useSubBlockStore } from '@/stores/workflow/subblock/store'
export function useSubBlockValue<T = any>(
blockId: string,
subBlockId: string
subBlockId: string,
triggerWorkflowUpdate: boolean = false
): readonly [T | null, (value: T) => void] {
const value = useWorkflowStore(
// Get initial value from workflow store
const initialValue = useWorkflowStore(
useCallback(
(state) => state.blocks[blockId]?.subBlocks[subBlockId]?.value ?? null,
[blockId, subBlockId]
)
)
const updateSubBlock = useWorkflowStore((state) => state.updateSubBlock)
// Get value and setter from subblock store
const value = useSubBlockStore(
useCallback(
(state) => state.getValue(blockId, subBlockId) ?? initialValue,
[blockId, subBlockId, initialValue]
)
)
const setValue = useCallback(
(newValue: T) => {
updateSubBlock(blockId, subBlockId, newValue as any)
useSubBlockStore.getState().setValue(blockId, subBlockId, newValue)
if (triggerWorkflowUpdate) {
useWorkflowStore.getState().triggerUpdate()
}
},
[blockId, subBlockId, updateSubBlock]
[blockId, subBlockId, triggerWorkflowUpdate]
)
return [value as T | null, setValue] as const

View File

@@ -8,6 +8,7 @@ import { Tooltip, TooltipContent, TooltipTrigger } from '@/components/ui/tooltip
import { cn } from '@/lib/utils'
import { useExecutionStore } from '@/stores/execution/store'
import { useWorkflowStore } from '@/stores/workflow/store'
import { mergeSubblockState } from '@/stores/workflow/utils'
import { BlockConfig, SubBlockConfig } from '@/blocks/types'
import { ActionBar } from './components/action-bar/action-bar'
import { ConnectionBlocks } from './components/connection-blocks/connection-blocks'
@@ -34,6 +35,7 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
const updateNodeInternals = useUpdateNodeInternals()
// Workflow store selectors
const lastUpdate = useWorkflowStore((state) => state.lastUpdate)
const isEnabled = useWorkflowStore((state) => state.blocks[id]?.enabled ?? true)
const horizontalHandles = useWorkflowStore(
(state) => state.blocks[id]?.horizontalHandles ?? false
@@ -100,7 +102,7 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
cancelAnimationFrame(rafId)
}
}
}, [id, blockHeight, updateBlockHeight, updateNodeInternals])
}, [id, blockHeight, updateBlockHeight, updateNodeInternals, lastUpdate])
// SubBlock layout management
function groupSubBlocks(subBlocks: SubBlockConfig[], blockId: string) {
@@ -108,6 +110,10 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
let currentRow: SubBlockConfig[] = []
let currentRowWidth = 0
// Get merged state for this block
const blocks = useWorkflowStore.getState().blocks
const mergedState = mergeSubblockState(blocks, blockId)[blockId]
// Filter visible blocks and those that meet their conditions
const visibleSubBlocks = subBlocks.filter((block) => {
if (block.hidden) return false
@@ -115,11 +121,10 @@ export function WorkflowBlock({ id, data, selected }: NodeProps<WorkflowBlockPro
// If there's no condition, the block should be shown
if (!block.condition) return true
// Get the values of the fields this block depends on
const fieldValue =
useWorkflowStore.getState().blocks[blockId]?.subBlocks[block.condition.field]?.value
// Get the values of the fields this block depends on from merged state
const fieldValue = mergedState?.subBlocks[block.condition.field]?.value
const andFieldValue = block.condition.and
? useWorkflowStore.getState().blocks[blockId]?.subBlocks[block.condition.and.field]?.value
? mergedState?.subBlocks[block.condition.and.field]?.value
: undefined
// Check both conditions if 'and' is present

View File

@@ -323,9 +323,9 @@ function WorkflowContent() {
}, [selectedEdgeId, removeEdge])
// Initialize state logging
useEffect(() => {
initializeStateLogger()
}, [])
// useEffect(() => {
// initializeStateLogger()
// }, [])
if (!isInitialized) return null

View File

@@ -4,6 +4,8 @@ import { useNotificationStore } from '@/stores/notifications/store'
import { useEnvironmentStore } from '@/stores/settings/environment/store'
import { useWorkflowRegistry } from '@/stores/workflow/registry/store'
import { useWorkflowStore } from '@/stores/workflow/store'
import { useSubBlockStore } from '@/stores/workflow/subblock/store'
import { mergeSubblockState } from '@/stores/workflow/utils'
import { Executor } from '@/executor'
import { ExecutionResult } from '@/executor/types'
import { Serializer } from '@/serializer'
@@ -27,18 +29,32 @@ export function useWorkflowExecution() {
}
try {
// Extract existing block states
const currentBlockStates = Object.entries(blocks).reduce(
// Use the mergeSubblockState utility to get all block states
const mergedStates = mergeSubblockState(blocks)
const currentBlockStates = Object.entries(mergedStates).reduce(
(acc, [id, block]) => {
const responseValue = block.subBlocks?.response?.value
if (responseValue !== undefined) {
acc[id] = { response: responseValue }
}
acc[id] = Object.entries(block.subBlocks).reduce(
(subAcc, [key, subBlock]) => {
subAcc[key] = subBlock.value
return subAcc
},
{} as Record<string, any>
)
return acc
},
{} as Record<string, any>
{} as Record<string, Record<string, any>>
)
// Debug logging
console.group('Workflow Execution State')
console.log('Block Configurations:', blocks)
console.log(
'SubBlock Store Values:',
useSubBlockStore.getState().workflowValues[activeWorkflowId]
)
console.log('Merged Block States for Execution:', currentBlockStates)
console.groupEnd()
// Get environment variables
const envVars = getAllVariables()
const envVarValues = Object.entries(envVars).reduce(
@@ -50,7 +66,7 @@ export function useWorkflowExecution() {
)
// Execute workflow
const workflow = new Serializer().serializeWorkflow(blocks, edges, loops)
const workflow = new Serializer().serializeWorkflow(mergedStates, edges, loops)
const executor = new Executor(workflow, currentBlockStates, envVarValues)
const result = await executor.execute(activeWorkflowId)
@@ -65,6 +81,7 @@ export function useWorkflowExecution() {
activeWorkflowId
)
} catch (error: any) {
console.error('Workflow Execution Error:', error)
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
setExecutionResult({
success: false,

View File

@@ -1,6 +1,7 @@
import { create } from 'zustand'
import { devtools } from 'zustand/middleware'
import { useWorkflowStore } from '../store'
import { useSubBlockStore } from '../subblock/store'
import { WorkflowMetadata, WorkflowRegistry } from './types'
import { generateUniqueName } from './utils'
@@ -36,10 +37,14 @@ export const useWorkflowRegistry = create<WorkflowRegistry>()(
)
}
// Load new workflow state
// Load workflow state
const savedState = localStorage.getItem(`workflow-${id}`)
if (savedState) {
const { blocks, edges, history, loops } = JSON.parse(savedState)
// Initialize subblock store with workflow values
useSubBlockStore.getState().initializeFromWorkflow(id, blocks)
useWorkflowStore.setState({
blocks,
edges,

View File

@@ -4,6 +4,8 @@ import { devtools } from 'zustand/middleware'
import { getBlock } from '@/blocks'
import { resolveOutputType } from '@/blocks/utils'
import { WorkflowStoreWithHistory, pushHistory, withHistory } from './middleware'
import { useWorkflowRegistry } from './registry/store'
import { useSubBlockStore } from './subblock/store'
import { Loop, Position, SubBlockState } from './types'
import { detectCycle } from './utils'
@@ -33,75 +35,6 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
canRedo: () => false,
revertToHistoryState: () => {},
updateSubBlock: (blockId: string, subBlockId: string, value: any) => {
set((state) => {
const block = state.blocks[blockId]
if (!block) return state
// Handle different value types appropriately
const processedValue = Array.isArray(value)
? value
: block.subBlocks[subBlockId]?.type === 'slider'
? Number(value) // Convert slider values to numbers
: typeof value === 'string'
? value
: JSON.stringify(value, null, 2)
// Only attempt JSON parsing for agent responseFormat validation
if (
block.type === 'agent' &&
subBlockId === 'responseFormat' &&
typeof processedValue === 'string'
) {
try {
// Parse the input string to validate JSON but keep original string value
const parsed = JSON.parse(processedValue)
// Simple validation of required schema structure
if (!parsed.fields || !Array.isArray(parsed.fields)) {
console.error('Validation failed: missing fields array')
throw new Error('Response format must have a fields array')
}
for (const field of parsed.fields) {
if (!field.name || !field.type) {
console.error('Validation failed: field missing name or type', field)
throw new Error('Each field must have a name and type')
}
if (!['string', 'number', 'boolean', 'array', 'object'].includes(field.type)) {
console.error('Validation failed: invalid field type', field)
throw new Error(
`Invalid type "${field.type}" - must be one of: string, number, boolean, array, object`
)
}
}
// Don't modify the value, keep it as the original string
} catch (error: any) {
console.error('responseFormat validation error:', error)
throw new Error(`Invalid JSON schema: ${error.message}`)
}
}
return {
blocks: {
...state.blocks,
[blockId]: {
...block,
subBlocks: {
...block.subBlocks,
[subBlockId]: {
...block.subBlocks[subBlockId],
value: processedValue,
},
},
},
},
}
})
get().updateLastSaved()
},
addBlock: (id: string, type: string, name: string, position: Position) => {
const blockConfig = getBlock(type)
if (!blockConfig) return
@@ -158,20 +91,38 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
},
removeBlock: (id: string) => {
// First, clean up any subblock values for this block
const subBlockStore = useSubBlockStore.getState()
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
const newState = {
blocks: { ...get().blocks },
edges: [...get().edges].filter((edge) => edge.source !== id && edge.target !== id),
loops: { ...get().loops },
}
// Remove the block from any loops that contain it
// Clean up subblock values before removing the block
if (activeWorkflowId) {
const updatedWorkflowValues = {
...(subBlockStore.workflowValues[activeWorkflowId] || {}),
}
delete updatedWorkflowValues[id]
// Update subblock store
useSubBlockStore.setState((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: updatedWorkflowValues,
},
}))
}
// Clean up loops
Object.entries(newState.loops).forEach(([loopId, loop]) => {
if (loop.nodes.includes(id)) {
// If the loop would only have 1 or 0 nodes after removal, delete the loop
if (loop.nodes.length <= 2) {
delete newState.loops[loopId]
} else {
// Otherwise, just remove the node from the loop
newState.loops[loopId] = {
...loop,
nodes: loop.nodes.filter((nodeId) => nodeId !== id),
@@ -180,7 +131,7 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
}
})
// Delete the block itself
// Delete the block last
delete newState.blocks[id]
set(newState)
@@ -450,6 +401,13 @@ export const useWorkflowStore = create<WorkflowStoreWithHistory>()(
pushHistory(set, get, newState, 'Update loop max iterations')
get().updateLastSaved()
},
triggerUpdate: () => {
set((state) => ({
...state,
lastUpdate: Date.now(),
}))
},
})),
{ name: 'workflow-store' }
)

View File

@@ -0,0 +1,123 @@
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'
import { SubBlockConfig } from '@/blocks/types'
import { useWorkflowRegistry } from '../registry/store'
interface SubBlockState {
workflowValues: Record<string, Record<string, Record<string, any>>> // Store values per workflow ID
}
interface SubBlockStore extends SubBlockState {
setValue: (blockId: string, subBlockId: string, value: any) => void
getValue: (blockId: string, subBlockId: string) => any
clear: () => void
initializeFromWorkflow: (workflowId: string, blocks: Record<string, any>) => void
}
export const useSubBlockStore = create<SubBlockStore>()(
devtools(
persist(
(set, get) => ({
workflowValues: {},
setValue: (blockId: string, subBlockId: string, value: any) => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
set((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {
...state.workflowValues[activeWorkflowId],
[blockId]: {
...state.workflowValues[activeWorkflowId]?.[blockId],
[subBlockId]: value,
},
},
},
}))
// Persist to localStorage for backup
const storageKey = `subblock-values-${activeWorkflowId}`
const currentValues = get().workflowValues[activeWorkflowId] || {}
localStorage.setItem(storageKey, JSON.stringify(currentValues))
},
getValue: (blockId: string, subBlockId: string) => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return null
return get().workflowValues[activeWorkflowId]?.[blockId]?.[subBlockId] ?? null
},
clear: () => {
const activeWorkflowId = useWorkflowRegistry.getState().activeWorkflowId
if (!activeWorkflowId) return
set((state) => ({
workflowValues: {
...state.workflowValues,
[activeWorkflowId]: {},
},
}))
localStorage.removeItem(`subblock-values-${activeWorkflowId}`)
},
initializeFromWorkflow: (workflowId: string, blocks: Record<string, any>) => {
// First, try to load from localStorage
const storageKey = `subblock-values-${workflowId}`
const savedValues = localStorage.getItem(storageKey)
if (savedValues) {
const parsedValues = JSON.parse(savedValues)
set((state) => ({
workflowValues: {
...state.workflowValues,
[workflowId]: parsedValues,
},
}))
return
}
// If no saved values, initialize from blocks
const values: Record<string, Record<string, any>> = {}
Object.entries(blocks).forEach(([blockId, block]) => {
values[blockId] = {}
Object.entries(block.subBlocks).forEach(([subBlockId, subBlock]) => {
values[blockId][subBlockId] = (subBlock as SubBlockConfig).value
})
})
set((state) => ({
workflowValues: {
...state.workflowValues,
[workflowId]: values,
},
}))
// Save to localStorage
localStorage.setItem(storageKey, JSON.stringify(values))
},
}),
{
name: 'subblock-store',
partialize: (state) => ({ workflowValues: state.workflowValues }),
// Use default storage
storage: {
getItem: (name) => {
const value = localStorage.getItem(name)
return value ? JSON.parse(value) : null
},
setItem: (name, value) => {
localStorage.setItem(name, JSON.stringify(value))
},
removeItem: (name) => {
localStorage.removeItem(name)
},
},
}
),
{ name: 'subblock-store' }
)
)

View File

@@ -36,12 +36,12 @@ export interface WorkflowState {
edges: Edge[]
lastSaved?: number
loops: Record<string, Loop>
lastUpdate?: number
}
export interface WorkflowActions {
addBlock: (id: string, type: string, name: string, position: Position) => void
updateBlockPosition: (id: string, position: Position) => void
updateSubBlock: (blockId: string, subBlockId: string, subBlock: SubBlockState) => void
removeBlock: (id: string) => void
addEdge: (edge: Edge) => void
removeEdge: (edgeId: string) => void
@@ -54,6 +54,7 @@ export interface WorkflowActions {
toggleBlockWide: (id: string) => void
updateBlockHeight: (id: string, height: number) => void
updateLoopMaxIterations: (loopId: string, maxIterations: number) => void
triggerUpdate: () => void
}
export type WorkflowStore = WorkflowState & WorkflowActions

View File

@@ -1,4 +1,49 @@
import { Edge } from 'reactflow'
import { useSubBlockStore } from './subblock/store'
import { BlockState, SubBlockState } from './types'
/**
* Merges workflow block states with subblock values while maintaining block structure
* @param blocks - Block configurations from workflow store
* @param blockId - Optional specific block ID to merge (merges all if not provided)
* @returns Merged block states with updated values
*/
export function mergeSubblockState(
blocks: Record<string, BlockState>,
blockId?: string
): Record<string, BlockState> {
const blocksToProcess = blockId ? { [blockId]: blocks[blockId] } : blocks
return Object.entries(blocksToProcess).reduce(
(acc, [id, block]) => {
// Create a deep copy of the block's subBlocks to maintain structure
const mergedSubBlocks = Object.entries(block.subBlocks).reduce(
(subAcc, [subBlockId, subBlock]) => {
// Get the stored value for this subblock
const storedValue = useSubBlockStore.getState().getValue(id, subBlockId)
// Create a new subblock object with the same structure but updated value
subAcc[subBlockId] = {
...subBlock,
value: storedValue !== undefined && storedValue !== null ? storedValue : subBlock.value,
}
return subAcc
},
{} as Record<string, SubBlockState>
)
// Return the full block state with updated subBlocks
acc[id] = {
...block,
subBlocks: mergedSubBlocks,
}
return acc
},
{} as Record<string, BlockState>
)
}
/**
* Performs a depth-first search to detect all cycles in the graph