diff --git a/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx b/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx index 10bb122da..073256746 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx +++ b/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx @@ -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(() => { diff --git a/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts b/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts index f4ec5be57..8fdae8d31 100644 --- a/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts +++ b/app/w/[id]/components/workflow-block/components/sub-block/hooks/use-sub-block-value.ts @@ -1,24 +1,36 @@ import { useCallback } from 'react' import { useWorkflowStore } from '@/stores/workflow/store' +import { useSubBlockStore } from '@/stores/workflow/subblock/store' export function useSubBlockValue( 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 diff --git a/app/w/[id]/components/workflow-block/workflow-block.tsx b/app/w/[id]/components/workflow-block/workflow-block.tsx index 9a1118ce7..18cfc6c83 100644 --- a/app/w/[id]/components/workflow-block/workflow-block.tsx +++ b/app/w/[id]/components/workflow-block/workflow-block.tsx @@ -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 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 { if (block.hidden) return false @@ -115,11 +121,10 @@ export function WorkflowBlock({ id, data, selected }: NodeProps { - initializeStateLogger() - }, []) + // useEffect(() => { + // initializeStateLogger() + // }, []) if (!isInitialized) return null diff --git a/app/w/hooks/use-workflow-execution.ts b/app/w/hooks/use-workflow-execution.ts index 526dbc918..7aa0bb1ac 100644 --- a/app/w/hooks/use-workflow-execution.ts +++ b/app/w/hooks/use-workflow-execution.ts @@ -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 + ) return acc }, - {} as Record + {} as Record> ) + // 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, diff --git a/stores/workflow/registry/store.ts b/stores/workflow/registry/store.ts index 3b3f0df37..3c25afee7 100644 --- a/stores/workflow/registry/store.ts +++ b/stores/workflow/registry/store.ts @@ -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()( ) } - // 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, diff --git a/stores/workflow/store.ts b/stores/workflow/store.ts index b8381a6af..6dfcf517f 100644 --- a/stores/workflow/store.ts +++ b/stores/workflow/store.ts @@ -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()( 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()( }, 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()( } }) - // Delete the block itself + // Delete the block last delete newState.blocks[id] set(newState) @@ -450,6 +401,13 @@ export const useWorkflowStore = create()( pushHistory(set, get, newState, 'Update loop max iterations') get().updateLastSaved() }, + + triggerUpdate: () => { + set((state) => ({ + ...state, + lastUpdate: Date.now(), + })) + }, })), { name: 'workflow-store' } ) diff --git a/stores/workflow/subblock/store.ts b/stores/workflow/subblock/store.ts new file mode 100644 index 000000000..235b29951 --- /dev/null +++ b/stores/workflow/subblock/store.ts @@ -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>> // 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) => void +} + +export const useSubBlockStore = create()( + 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) => { + // 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> = {} + 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' } + ) +) diff --git a/stores/workflow/types.ts b/stores/workflow/types.ts index 5847f7cf2..14ce32ac6 100644 --- a/stores/workflow/types.ts +++ b/stores/workflow/types.ts @@ -36,12 +36,12 @@ export interface WorkflowState { edges: Edge[] lastSaved?: number loops: Record + 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 diff --git a/stores/workflow/utils.ts b/stores/workflow/utils.ts index ca9487219..b6005b2a0 100644 --- a/stores/workflow/utils.ts +++ b/stores/workflow/utils.ts @@ -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, + blockId?: string +): Record { + 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 + ) + + // Return the full block state with updated subBlocks + acc[id] = { + ...block, + subBlocks: mergedSubBlocks, + } + + return acc + }, + {} as Record + ) +} /** * Performs a depth-first search to detect all cycles in the graph