11'use client'
22
3- import { useEffect , useMemo , useRef , useState } from 'react'
4- import { Check } from 'lucide-react'
3+ import type React from 'react'
4+ import { useCallback , useEffect , useMemo , useRef , useState } from 'react'
5+ import { Check , RepeatIcon , SplitIcon } from 'lucide-react'
56import {
67 Badge ,
78 Popover ,
@@ -19,6 +20,32 @@ import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
1920import { useSubBlockStore } from '@/stores/workflows/subblock/store'
2021import { useWorkflowStore } from '@/stores/workflows/workflow/store'
2122
23+ /**
24+ * Renders a tag icon with background color.
25+ *
26+ * @param icon - Either a letter string or a Lucide icon component
27+ * @param color - Background color for the icon container
28+ * @returns A styled icon element
29+ */
30+ const TagIcon : React . FC < {
31+ icon : string | React . ComponentType < { className ?: string } >
32+ color : string
33+ } > = ( { icon, color } ) => (
34+ < div
35+ className = 'flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
36+ style = { { background : color } }
37+ >
38+ { typeof icon === 'string' ? (
39+ < span className = '!text-white font-bold text-[10px]' > { icon } </ span >
40+ ) : (
41+ ( ( ) => {
42+ const IconComponent = icon
43+ return < IconComponent className = '!text-white size-[9px]' />
44+ } ) ( )
45+ ) }
46+ </ div >
47+ )
48+
2249/**
2350 * Props for the OutputSelect component
2451 */
@@ -71,7 +98,6 @@ export function OutputSelect({
7198 const [ highlightedIndex , setHighlightedIndex ] = useState ( - 1 )
7299 const triggerRef = useRef < HTMLDivElement > ( null )
73100 const popoverRef = useRef < HTMLDivElement > ( null )
74- const contentRef = useRef < HTMLDivElement > ( null )
75101 const blocks = useWorkflowStore ( ( state ) => state . blocks )
76102 const { isShowingDiff, isDiffReady, hasActiveDiff, baselineWorkflow } = useWorkflowDiffStore ( )
77103 const subBlockValues = useSubBlockStore ( ( state ) =>
@@ -185,8 +211,11 @@ export function OutputSelect({
185211 * @param o - The output object to check
186212 * @returns True if the output is selected, false otherwise
187213 */
188- const isSelectedValue = ( o : { id : string ; label : string } ) =>
189- selectedOutputs . includes ( o . id ) || selectedOutputs . includes ( o . label )
214+ const isSelectedValue = useCallback (
215+ ( o : { id : string ; label : string } ) =>
216+ selectedOutputs . includes ( o . id ) || selectedOutputs . includes ( o . label ) ,
217+ [ selectedOutputs ]
218+ )
190219
191220 /**
192221 * Gets display text for selected outputs
@@ -292,82 +321,96 @@ export function OutputSelect({
292321 * Handles output selection by toggling the selected state
293322 * @param value - The output label to toggle
294323 */
295- const handleOutputSelection = ( value : string ) => {
296- const emittedValue =
297- valueMode === 'label' ? value : workflowOutputs . find ( ( o ) => o . label === value ) ?. id || value
298- const index = selectedOutputs . indexOf ( emittedValue )
299-
300- const newSelectedOutputs =
301- index === - 1
302- ? [ ...new Set ( [ ...selectedOutputs , emittedValue ] ) ]
303- : selectedOutputs . filter ( ( id ) => id !== emittedValue )
304-
305- onOutputSelect ( newSelectedOutputs )
306- }
324+ const handleOutputSelection = useCallback (
325+ ( value : string ) => {
326+ const emittedValue =
327+ valueMode === 'label' ? value : workflowOutputs . find ( ( o ) => o . label === value ) ?. id || value
328+ const index = selectedOutputs . indexOf ( emittedValue )
329+
330+ const newSelectedOutputs =
331+ index === - 1
332+ ? [ ...new Set ( [ ...selectedOutputs , emittedValue ] ) ]
333+ : selectedOutputs . filter ( ( id ) => id !== emittedValue )
334+
335+ onOutputSelect ( newSelectedOutputs )
336+ } ,
337+ [ valueMode , workflowOutputs , selectedOutputs , onOutputSelect ]
338+ )
307339
308340 /**
309341 * Handles keyboard navigation within the output list
310342 * Supports ArrowUp, ArrowDown, Enter, and Escape keys
311- * @param e - Keyboard event
312343 */
313- const handleKeyDown = ( e : React . KeyboardEvent ) => {
314- if ( flattenedOutputs . length === 0 ) return
315-
316- switch ( e . key ) {
317- case 'ArrowDown' :
318- e . preventDefault ( )
319- setHighlightedIndex ( ( prev ) => {
320- const next = prev < flattenedOutputs . length - 1 ? prev + 1 : 0
321- return next
322- } )
323- break
324-
325- case 'ArrowUp' :
326- e . preventDefault ( )
327- setHighlightedIndex ( ( prev ) => {
328- const next = prev > 0 ? prev - 1 : flattenedOutputs . length - 1
329- return next
330- } )
331- break
332-
333- case 'Enter' :
334- e . preventDefault ( )
335- if ( highlightedIndex >= 0 && highlightedIndex < flattenedOutputs . length ) {
336- handleOutputSelection ( flattenedOutputs [ highlightedIndex ] . label )
337- }
338- break
344+ useEffect ( ( ) => {
345+ if ( ! open || flattenedOutputs . length === 0 ) return
346+
347+ const handleKeyboardEvent = ( e : KeyboardEvent ) => {
348+ switch ( e . key ) {
349+ case 'ArrowDown' :
350+ e . preventDefault ( )
351+ e . stopPropagation ( )
352+ setHighlightedIndex ( ( prev ) => {
353+ // If no selection or at end, go to start. Otherwise increment
354+ if ( prev === - 1 || prev >= flattenedOutputs . length - 1 ) {
355+ return 0
356+ }
357+ return prev + 1
358+ } )
359+ break
360+
361+ case 'ArrowUp' :
362+ e . preventDefault ( )
363+ e . stopPropagation ( )
364+ setHighlightedIndex ( ( prev ) => {
365+ // If no selection or at start, go to end. Otherwise decrement
366+ if ( prev <= 0 ) {
367+ return flattenedOutputs . length - 1
368+ }
369+ return prev - 1
370+ } )
371+ break
372+
373+ case 'Enter' :
374+ e . preventDefault ( )
375+ e . stopPropagation ( )
376+ setHighlightedIndex ( ( currentIndex ) => {
377+ if ( currentIndex >= 0 && currentIndex < flattenedOutputs . length ) {
378+ handleOutputSelection ( flattenedOutputs [ currentIndex ] . label )
379+ }
380+ return currentIndex
381+ } )
382+ break
339383
340- case 'Escape' :
341- e . preventDefault ( )
342- setOpen ( false )
343- break
384+ case 'Escape' :
385+ e . preventDefault ( )
386+ e . stopPropagation ( )
387+ setOpen ( false )
388+ break
389+ }
344390 }
345- }
391+
392+ window . addEventListener ( 'keydown' , handleKeyboardEvent , true )
393+ return ( ) => window . removeEventListener ( 'keydown' , handleKeyboardEvent , true )
394+ } , [ open , flattenedOutputs , handleOutputSelection ] )
346395
347396 /**
348397 * Reset highlighted index when popover opens/closes
349398 */
350399 useEffect ( ( ) => {
351400 if ( open ) {
352- // Find first selected item, or start at -1
353401 const firstSelectedIndex = flattenedOutputs . findIndex ( ( output ) => isSelectedValue ( output ) )
354402 setHighlightedIndex ( firstSelectedIndex >= 0 ? firstSelectedIndex : - 1 )
355-
356- // Focus the content for keyboard navigation
357- setTimeout ( ( ) => {
358- contentRef . current ?. focus ( )
359- } , 0 )
360403 } else {
361404 setHighlightedIndex ( - 1 )
362405 }
363- } , [ open , flattenedOutputs ] )
406+ } , [ open , flattenedOutputs , isSelectedValue ] )
364407
365408 /**
366409 * Scroll highlighted item into view
367410 */
368411 useEffect ( ( ) => {
369- if ( highlightedIndex >= 0 && contentRef . current ) {
370- const highlightedElement = contentRef . current . querySelector (
412+ if ( highlightedIndex >= 0 && popoverRef . current ) {
413+ const highlightedElement = popoverRef . current . querySelector (
371414 `[data-option-index="${ highlightedIndex } "]`
372415 )
373416 if ( highlightedElement ) {
@@ -425,18 +468,37 @@ export function OutputSelect({
425468 minWidth = { 160 }
426469 border
427470 disablePortal = { disablePopoverPortal }
428- onKeyDown = { handleKeyDown }
429- tabIndex = { 0 }
430- style = { { outline : 'none' } }
431471 >
432- < div ref = { contentRef } className = 'space-y-[2px]' >
472+ < div className = 'space-y-[2px]' >
433473 { Object . entries ( groupedOutputs ) . map ( ( [ blockName , outputs ] ) => {
434474 // Calculate the starting index for this group
435475 const startIndex = flattenedOutputs . findIndex ( ( o ) => o . blockName === blockName )
436476
477+ const firstOutput = outputs [ 0 ]
478+ const blockConfig = getBlock ( firstOutput . blockType )
479+ const blockColor = getOutputColor ( firstOutput . blockId , firstOutput . blockType )
480+
481+ // Determine the icon to use
482+ let blockIcon : string | React . ComponentType < { className ?: string } > = blockName
483+ . charAt ( 0 )
484+ . toUpperCase ( )
485+
486+ if ( blockConfig ?. icon ) {
487+ blockIcon = blockConfig . icon
488+ } else if ( firstOutput . blockType === 'loop' ) {
489+ blockIcon = RepeatIcon
490+ } else if ( firstOutput . blockType === 'parallel' ) {
491+ blockIcon = SplitIcon
492+ }
493+
437494 return (
438495 < div key = { blockName } >
439- < PopoverSection > { blockName } </ PopoverSection >
496+ < PopoverSection >
497+ < div className = 'flex items-center gap-1.5' >
498+ < TagIcon icon = { blockIcon } color = { blockColor } />
499+ < span > { blockName } </ span >
500+ </ div >
501+ </ PopoverSection >
440502
441503 < div className = 'flex flex-col gap-[2px]' >
442504 { outputs . map ( ( output , localIndex ) => {
@@ -451,17 +513,9 @@ export function OutputSelect({
451513 onClick = { ( ) => handleOutputSelection ( output . label ) }
452514 onMouseEnter = { ( ) => setHighlightedIndex ( globalIndex ) }
453515 >
454- < div
455- className = 'flex h-[14px] w-[14px] flex-shrink-0 items-center justify-center rounded'
456- style = { {
457- backgroundColor : getOutputColor ( output . blockId , output . blockType ) ,
458- } }
459- >
460- < span className = 'font-bold text-[10px] text-white' >
461- { blockName . charAt ( 0 ) . toUpperCase ( ) }
462- </ span >
463- </ div >
464- < span className = 'min-w-0 flex-1 truncate' > { output . path } </ span >
516+ < span className = 'min-w-0 flex-1 truncate text-[var(--text-primary)]' >
517+ { output . path }
518+ </ span >
465519 { isSelectedValue ( output ) && < Check className = 'h-3 w-3 flex-shrink-0' /> }
466520 </ PopoverItem >
467521 )
0 commit comments