88 DropdownMenuItem ,
99 DropdownMenuTrigger ,
1010} from '@/components/ui/dropdown-menu'
11+ import { formatDisplayText } from '@/components/ui/formatted-text'
1112import { Input } from '@/components/ui/input'
1213import { Label } from '@/components/ui/label'
1314import {
@@ -17,6 +18,7 @@ import {
1718 SelectTrigger ,
1819 SelectValue ,
1920} from '@/components/ui/select'
21+ import { checkTagTrigger , TagDropdown } from '@/components/ui/tag-dropdown'
2022import { Textarea } from '@/components/ui/textarea'
2123import { cn } from '@/lib/utils'
2224import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
@@ -72,7 +74,12 @@ export function FieldFormat({
7274 const [ storeValue , setStoreValue ] = useSubBlockValue < Field [ ] > ( blockId , subBlockId )
7375 const [ dragHighlight , setDragHighlight ] = useState < Record < string , boolean > > ( { } )
7476 const valueInputRefs = useRef < Record < string , HTMLInputElement | HTMLTextAreaElement > > ( { } )
77+ const overlayRefs = useRef < Record < string , HTMLDivElement > > ( { } )
7578 const [ localValues , setLocalValues ] = useState < Record < string , string > > ( { } )
79+ const [ showTags , setShowTags ] = useState ( false )
80+ const [ cursorPosition , setCursorPosition ] = useState ( 0 )
81+ const [ activeFieldId , setActiveFieldId ] = useState < string | null > ( null )
82+ const [ activeSourceBlockId , setActiveSourceBlockId ] = useState < string | null > ( null )
7683
7784 // Use preview value when in preview mode, otherwise use store value
7885 const value = isPreview ? previewValue : storeValue
@@ -106,18 +113,19 @@ export function FieldFormat({
106113 setStoreValue ( ( fields || [ ] ) . filter ( ( field : Field ) => field . id !== id ) )
107114 }
108115
109- // Validate field name for API safety
110116 const validateFieldName = ( name : string ) : string => {
111- // Remove only truly problematic characters for JSON/API usage
112- // Allow most characters but remove control characters, quotes, and backslashes
113117 return name . replace ( / [ \x00 - \x1F " \\ ] / g, '' ) . trim ( )
114118 }
115119
116- const handleValueInputChange = ( fieldId : string , newValue : string ) => {
120+ const handleValueInputChange = ( fieldId : string , newValue : string , caretPosition ?: number ) => {
117121 setLocalValues ( ( prev ) => ( { ...prev , [ fieldId ] : newValue } ) )
118- }
119122
120- // Value normalization: keep it simple for string types
123+ const position = typeof caretPosition === 'number' ? caretPosition : newValue . length
124+ setCursorPosition ( position )
125+ setActiveFieldId ( fieldId )
126+ const trigger = checkTagTrigger ( newValue , position )
127+ setShowTags ( trigger . show )
128+ }
121129
122130 const handleValueInputBlur = ( field : Field ) => {
123131 if ( isPreview || disabled ) return
@@ -148,6 +156,47 @@ export function FieldFormat({
148156 setDragHighlight ( ( prev ) => ( { ...prev , [ fieldId ] : false } ) )
149157 const input = valueInputRefs . current [ fieldId ]
150158 input ?. focus ( )
159+
160+ if ( input ) {
161+ const currentValue =
162+ localValues [ fieldId ] ?? ( fields . find ( ( f ) => f . id === fieldId ) ?. value as string ) ?? ''
163+ const dropPosition = ( input as any ) . selectionStart ?? currentValue . length
164+ const newValue = `${ currentValue . slice ( 0 , dropPosition ) } <${ currentValue . slice ( dropPosition ) } `
165+ setLocalValues ( ( prev ) => ( { ...prev , [ fieldId ] : newValue } ) )
166+ setActiveFieldId ( fieldId )
167+ setCursorPosition ( dropPosition + 1 )
168+ setShowTags ( true )
169+
170+ try {
171+ const data = JSON . parse ( e . dataTransfer . getData ( 'application/json' ) )
172+ if ( data ?. connectionData ?. sourceBlockId ) {
173+ setActiveSourceBlockId ( data . connectionData . sourceBlockId )
174+ }
175+ } catch { }
176+
177+ setTimeout ( ( ) => {
178+ const el = valueInputRefs . current [ fieldId ]
179+ if ( el && typeof ( el as any ) . selectionStart === 'number' ) {
180+ ; ( el as any ) . selectionStart = dropPosition + 1
181+ ; ( el as any ) . selectionEnd = dropPosition + 1
182+ }
183+ } , 0 )
184+ }
185+ }
186+
187+ const handleValueScroll = ( fieldId : string , e : React . UIEvent < HTMLInputElement > ) => {
188+ const overlay = overlayRefs . current [ fieldId ]
189+ if ( overlay ) {
190+ overlay . scrollLeft = e . currentTarget . scrollLeft
191+ }
192+ }
193+
194+ const handleValuePaste = ( fieldId : string ) => {
195+ setTimeout ( ( ) => {
196+ const input = valueInputRefs . current [ fieldId ] as HTMLInputElement | undefined
197+ const overlay = overlayRefs . current [ fieldId ]
198+ if ( input && overlay ) overlay . scrollLeft = input . scrollLeft
199+ } , 0 )
151200 }
152201
153202 // Update handlers
@@ -351,7 +400,13 @@ export function FieldFormat({
351400 } }
352401 name = 'value'
353402 value = { localValues [ field . id ] ?? ( field . value as string ) ?? '' }
354- onChange = { ( e ) => handleValueInputChange ( field . id , e . target . value ) }
403+ onChange = { ( e ) =>
404+ handleValueInputChange (
405+ field . id ,
406+ e . target . value ,
407+ e . target . selectionStart ?? undefined
408+ )
409+ }
355410 onBlur = { ( ) => handleValueInputBlur ( field ) }
356411 placeholder = {
357412 field . type === 'object' ? '{\n "key": "value"\n}' : '[\n 1, 2, 3\n]'
@@ -364,30 +419,80 @@ export function FieldFormat({
364419 config ?. connectionDroppable !== false &&
365420 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
366421 ) }
367- />
368- ) : (
369- < Input
370- ref = { ( el ) => {
371- if ( el ) valueInputRefs . current [ field . id ] = el
372- } }
373- name = 'value'
374- value = { localValues [ field . id ] ?? field . value ?? '' }
375- onChange = { ( e ) => handleValueInputChange ( field . id , e . target . value ) }
376- onBlur = { ( ) => handleValueInputBlur ( field ) }
377- onDragOver = { ( e ) => handleDragOver ( e , field . id ) }
378- onDragLeave = { ( e ) => handleDragLeave ( e , field . id ) }
379422 onDrop = { ( e ) => handleDrop ( e , field . id ) }
380- placeholder = { valuePlaceholder }
381- disabled = { isPreview || disabled }
382- className = { cn (
383- 'h-9 placeholder:text-muted-foreground/50' ,
384- dragHighlight [ field . id ] && 'ring-2 ring-blue-500 ring-offset-2' ,
385- isConnecting &&
386- config ?. connectionDroppable !== false &&
387- 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
388- ) }
423+ onDragOver = { ( e ) =>
424+ handleDragOver ( e as unknown as React . DragEvent , field . id )
425+ }
426+ onDragLeave = { ( e ) =>
427+ handleDragLeave ( e as unknown as React . DragEvent , field . id )
428+ }
389429 />
430+ ) : (
431+ < >
432+ < Input
433+ ref = { ( el ) => {
434+ if ( el ) valueInputRefs . current [ field . id ] = el
435+ } }
436+ name = 'value'
437+ value = { localValues [ field . id ] ?? field . value ?? '' }
438+ onChange = { ( e ) =>
439+ handleValueInputChange (
440+ field . id ,
441+ e . target . value ,
442+ e . target . selectionStart ?? undefined
443+ )
444+ }
445+ onBlur = { ( ) => handleValueInputBlur ( field ) }
446+ onDragOver = { ( e ) => handleDragOver ( e , field . id ) }
447+ onDragLeave = { ( e ) => handleDragLeave ( e , field . id ) }
448+ onDrop = { ( e ) => handleDrop ( e , field . id ) }
449+ onScroll = { ( e ) => handleValueScroll ( field . id , e ) }
450+ onPaste = { ( ) => handleValuePaste ( field . id ) }
451+ placeholder = { valuePlaceholder }
452+ disabled = { isPreview || disabled }
453+ className = { cn (
454+ 'allow-scroll h-9 w-full overflow-auto text-transparent caret-foreground placeholder:text-muted-foreground/50' ,
455+ dragHighlight [ field . id ] && 'ring-2 ring-blue-500 ring-offset-2' ,
456+ isConnecting &&
457+ config ?. connectionDroppable !== false &&
458+ 'ring-2 ring-blue-500 ring-offset-2 focus-visible:ring-blue-500'
459+ ) }
460+ style = { { overflowX : 'auto' } }
461+ />
462+ < div
463+ ref = { ( el ) => {
464+ if ( el ) overlayRefs . current [ field . id ] = el
465+ } }
466+ className = 'pointer-events-none absolute inset-0 flex items-center overflow-x-auto bg-transparent px-3 text-sm'
467+ style = { { overflowX : 'auto' } }
468+ >
469+ < div
470+ className = 'w-full whitespace-pre'
471+ style = { { scrollbarWidth : 'none' , minWidth : 'fit-content' } }
472+ >
473+ { formatDisplayText (
474+ ( localValues [ field . id ] ?? field . value ?? '' ) ?. toString ( ) ,
475+ true
476+ ) }
477+ </ div >
478+ </ div >
479+ </ >
390480 ) }
481+ { /* Tag dropdown for response value field */ }
482+ < TagDropdown
483+ visible = { showTags && activeFieldId === field . id }
484+ onSelect = { ( newValue ) => {
485+ setLocalValues ( ( prev ) => ( { ...prev , [ field . id ] : newValue } ) )
486+ if ( ! isPreview && ! disabled ) updateField ( field . id , 'value' , newValue )
487+ setShowTags ( false )
488+ setActiveSourceBlockId ( null )
489+ } }
490+ blockId = { blockId }
491+ activeSourceBlockId = { activeSourceBlockId }
492+ inputValue = { localValues [ field . id ] ?? ( field . value as string ) ?? '' }
493+ cursorPosition = { cursorPosition }
494+ onClose = { ( ) => setShowTags ( false ) }
495+ />
391496 </ div >
392497 </ div >
393498 ) }
0 commit comments