11import { useCallback , useEffect , useRef } from 'react'
22import { isEqual } from 'lodash'
3+ import { createLogger } from '@/lib/logs/console-logger'
34import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
45import { getProviderFromModel } from '@/providers/utils'
56import { useGeneralStore } from '@/stores/settings/general/store'
7+ import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
68import { useSubBlockStore } from '@/stores/workflows/subblock/store'
79import { useWorkflowStore } from '@/stores/workflows/workflow/store'
810
11+ const logger = createLogger ( 'SubBlockValue' )
12+
913// Helper function to dispatch collaborative subblock updates
1014const dispatchSubblockUpdate = ( blockId : string , subBlockId : string , value : any ) => {
1115 const event = new CustomEvent ( 'update-subblock-value' , {
@@ -154,20 +158,31 @@ function storeApiKeyValue(
154158 }
155159}
156160
161+ interface UseSubBlockValueOptions {
162+ debounceMs ?: number
163+ isStreaming ?: boolean // Explicit streaming state
164+ onStreamingEnd ?: ( ) => void
165+ }
166+
157167/**
158168 * Custom hook to get and set values for a sub-block in a workflow.
159169 * Handles complex object values properly by using deep equality comparison.
170+ * Includes automatic debouncing and explicit streaming mode for AI generation.
160171 *
161172 * @param blockId The ID of the block containing the sub-block
162173 * @param subBlockId The ID of the sub-block
163174 * @param triggerWorkflowUpdate Whether to trigger a workflow update when the value changes
164- * @returns A tuple containing the current value and a setter function
175+ * @param options Configuration for debouncing and streaming behavior
176+ * @returns A tuple containing the current value and setter function
165177 */
166178export function useSubBlockValue < T = any > (
167179 blockId : string ,
168180 subBlockId : string ,
169- triggerWorkflowUpdate = false
181+ triggerWorkflowUpdate = false ,
182+ options ?: UseSubBlockValueOptions
170183) : readonly [ T | null , ( value : T ) => void ] {
184+ const { debounceMs = 150 , isStreaming = false , onStreamingEnd } = options || { }
185+
171186 const { collaborativeSetSubblockValue } = useCollaborativeWorkflow ( )
172187
173188 const blockType = useWorkflowStore (
@@ -187,6 +202,12 @@ export function useSubBlockValue<T = any>(
187202 // Previous model reference for detecting model changes
188203 const prevModelRef = useRef < string | null > ( null )
189204
205+ // Debouncing refs
206+ const debounceTimerRef = useRef < NodeJS . Timeout | null > ( null )
207+ const lastEmittedValueRef = useRef < T | null > ( null )
208+ const streamingValueRef = useRef < T | null > ( null )
209+ const wasStreamingRef = useRef < boolean > ( false )
210+
190211 // Get value from subblock store - always call this hook unconditionally
191212 const storeValue = useSubBlockStore (
192213 useCallback ( ( state ) => state . getValue ( blockId , subBlockId ) , [ blockId , subBlockId ] )
@@ -211,13 +232,59 @@ export function useSubBlockValue<T = any>(
211232 // Compute the modelValue based on block type
212233 const modelValue = isProviderBasedBlock ? ( modelSubBlockValue as string ) : null
213234
235+ // Cleanup timer on unmount
236+ useEffect ( ( ) => {
237+ return ( ) => {
238+ if ( debounceTimerRef . current ) {
239+ clearTimeout ( debounceTimerRef . current )
240+ }
241+ }
242+ } , [ ] )
243+
244+ // Emit the value to socket/DB
245+ const emitValue = useCallback (
246+ ( value : T ) => {
247+ collaborativeSetSubblockValue ( blockId , subBlockId , value )
248+ lastEmittedValueRef . current = value
249+ } ,
250+ [ blockId , subBlockId , collaborativeSetSubblockValue ]
251+ )
252+
253+ // Handle streaming mode changes
254+ useEffect ( ( ) => {
255+ // If we just exited streaming mode, emit the final value
256+ if ( wasStreamingRef . current && ! isStreaming && streamingValueRef . current !== null ) {
257+ logger . debug ( 'Streaming ended, persisting final value' , { blockId, subBlockId } )
258+ emitValue ( streamingValueRef . current )
259+ streamingValueRef . current = null
260+ onStreamingEnd ?.( )
261+ }
262+ wasStreamingRef . current = isStreaming
263+ } , [ isStreaming , blockId , subBlockId , emitValue , onStreamingEnd ] )
264+
214265 // Hook to set a value in the subblock store
215266 const setValue = useCallback (
216267 ( newValue : T ) => {
217268 // Use deep comparison to avoid unnecessary updates for complex objects
218269 if ( ! isEqual ( valueRef . current , newValue ) ) {
219270 valueRef . current = newValue
220271
272+ // Always update local store immediately for UI responsiveness
273+ useSubBlockStore . setState ( ( state ) => ( {
274+ workflowValues : {
275+ ...state . workflowValues ,
276+ [ useWorkflowRegistry . getState ( ) . activeWorkflowId || '' ] : {
277+ ...state . workflowValues [ useWorkflowRegistry . getState ( ) . activeWorkflowId || '' ] ,
278+ [ blockId ] : {
279+ ...state . workflowValues [ useWorkflowRegistry . getState ( ) . activeWorkflowId || '' ] ?. [
280+ blockId
281+ ] ,
282+ [ subBlockId ] : newValue ,
283+ } ,
284+ } ,
285+ } ,
286+ } ) )
287+
221288 // Ensure we're passing the actual value, not a reference that might change
222289 const valueCopy =
223290 newValue === null
@@ -231,8 +298,27 @@ export function useSubBlockValue<T = any>(
231298 storeApiKeyValue ( blockId , blockType , modelValue , newValue , storeValue )
232299 }
233300
234- // Use collaborative function which handles both local store update and socket emission
235- collaborativeSetSubblockValue ( blockId , subBlockId , valueCopy )
301+ // Clear any existing debounce timer
302+ if ( debounceTimerRef . current ) {
303+ clearTimeout ( debounceTimerRef . current )
304+ debounceTimerRef . current = null
305+ }
306+
307+ // If streaming, just store the value without emitting
308+ if ( isStreaming ) {
309+ streamingValueRef . current = valueCopy
310+ } else {
311+ // Detect large changes for extended debounce
312+ const isLargeChange = detectLargeChange ( lastEmittedValueRef . current , valueCopy )
313+ const effectiveDebounceMs = isLargeChange ? debounceMs * 2 : debounceMs
314+
315+ // Debounce the socket emission
316+ debounceTimerRef . current = setTimeout ( ( ) => {
317+ if ( valueRef . current !== null && valueRef . current !== lastEmittedValueRef . current ) {
318+ emitValue ( valueCopy )
319+ }
320+ } , effectiveDebounceMs )
321+ }
236322
237323 if ( triggerWorkflowUpdate ) {
238324 useWorkflowStore . getState ( ) . triggerUpdate ( )
@@ -247,7 +333,9 @@ export function useSubBlockValue<T = any>(
247333 storeValue ,
248334 triggerWorkflowUpdate ,
249335 modelValue ,
250- collaborativeSetSubblockValue ,
336+ isStreaming ,
337+ debounceMs ,
338+ emitValue ,
251339 ]
252340 )
253341
@@ -320,5 +408,29 @@ export function useSubBlockValue<T = any>(
320408 }
321409 } , [ storeValue , initialValue ] )
322410
411+ // Return appropriate tuple based on whether options were provided
323412 return [ storeValue !== undefined ? storeValue : initialValue , setValue ] as const
324413}
414+
415+ // Helper function to detect large changes
416+ function detectLargeChange ( oldValue : any , newValue : any ) : boolean {
417+ // Handle null/undefined
418+ if ( oldValue == null && newValue == null ) return false
419+ if ( oldValue == null || newValue == null ) return true
420+
421+ // For strings, check if it's a large paste or deletion
422+ if ( typeof oldValue === 'string' && typeof newValue === 'string' ) {
423+ const sizeDiff = Math . abs ( newValue . length - oldValue . length )
424+ // Consider it a large change if more than 50 characters changed at once
425+ return sizeDiff > 50
426+ }
427+
428+ // For arrays, check length difference
429+ if ( Array . isArray ( oldValue ) && Array . isArray ( newValue ) ) {
430+ const sizeDiff = Math . abs ( newValue . length - oldValue . length )
431+ return sizeDiff > 5
432+ }
433+
434+ // For other types, always treat as small change
435+ return false
436+ }
0 commit comments