1- import { useMemo } from 'react'
1+ import { useMemo , useRef , useState } from 'react'
22import { Plus , Trash } from 'lucide-react'
33import { Button } from '@/components/ui/button'
4+ import { formatDisplayText } from '@/components/ui/formatted-text'
45import { Input } from '@/components/ui/input'
56import { Label } from '@/components/ui/label'
7+ import { checkTagTrigger , TagDropdown } from '@/components/ui/tag-dropdown'
8+ import { Textarea } from '@/components/ui/textarea'
69import { Tooltip , TooltipContent , TooltipTrigger } from '@/components/ui/tooltip'
10+ import { cn } from '@/lib/utils'
711import { useSubBlockValue } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/workflow-block/components/sub-block/hooks/use-sub-block-value'
12+ import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
813
914interface EvalMetric {
1015 id : string
@@ -22,6 +27,7 @@ interface EvalInputProps {
2227 isPreview ?: boolean
2328 previewValue ?: EvalMetric [ ] | null
2429 disabled ?: boolean
30+ isConnecting ?: boolean
2531}
2632
2733// Default values
@@ -38,17 +44,24 @@ export function EvalInput({
3844 isPreview = false ,
3945 previewValue,
4046 disabled = false ,
47+ isConnecting = false ,
4148} : EvalInputProps ) {
4249 const [ storeValue , setStoreValue ] = useSubBlockValue < EvalMetric [ ] > ( blockId , subBlockId )
50+ const accessiblePrefixes = useAccessibleReferencePrefixes ( blockId )
51+
52+ const [ showTags , setShowTags ] = useState ( false )
53+ const [ cursorPosition , setCursorPosition ] = useState ( 0 )
54+ const [ activeMetricId , setActiveMetricId ] = useState < string | null > ( null )
55+ const [ activeSourceBlockId , setActiveSourceBlockId ] = useState < string | null > ( null )
56+ const descriptionInputRefs = useRef < Record < string , HTMLTextAreaElement > > ( { } )
57+ const descriptionOverlayRefs = useRef < Record < string , HTMLDivElement > > ( { } )
58+ const [ dragHighlight , setDragHighlight ] = useState < Record < string , boolean > > ( { } )
4359
44- // Use preview value when in preview mode, otherwise use store value
4560 const value = isPreview ? previewValue : storeValue
4661
47- // State hooks - memoize default metric to prevent key changes
4862 const defaultMetric = useMemo ( ( ) => createDefaultMetric ( ) , [ ] )
4963 const metrics : EvalMetric [ ] = value || [ defaultMetric ]
5064
51- // Metric operations
5265 const addMetric = ( ) => {
5366 if ( isPreview || disabled ) return
5467
@@ -61,7 +74,6 @@ export function EvalInput({
6174 setStoreValue ( metrics . filter ( ( metric ) => metric . id !== id ) )
6275 }
6376
64- // Update handlers
6577 const updateMetric = ( id : string , field : keyof EvalMetric , value : any ) => {
6678 if ( isPreview || disabled ) return
6779 setStoreValue (
@@ -86,7 +98,6 @@ export function EvalInput({
8698 )
8799 }
88100
89- // Validation handlers
90101 const handleRangeBlur = ( id : string , field : 'min' | 'max' , value : string ) => {
91102 const sanitizedValue = value . replace ( / [ ^ \d . - ] / g, '' )
92103 const numValue = Number . parseFloat ( sanitizedValue )
@@ -106,7 +117,97 @@ export function EvalInput({
106117 )
107118 }
108119
109- // Metric header
120+ const handleTagSelect = ( tag : string ) => {
121+ if ( ! activeMetricId ) return
122+
123+ const metric = metrics . find ( ( m ) => m . id === activeMetricId )
124+ if ( ! metric ) return
125+
126+ const currentValue = metric . description || ''
127+ const textBeforeCursor = currentValue . slice ( 0 , cursorPosition )
128+ const lastOpenBracket = textBeforeCursor . lastIndexOf ( '<' )
129+
130+ const newValue =
131+ currentValue . slice ( 0 , lastOpenBracket ) + tag + currentValue . slice ( cursorPosition )
132+
133+ updateMetric ( activeMetricId , 'description' , newValue )
134+ setShowTags ( false )
135+
136+ setTimeout ( ( ) => {
137+ const inputEl = descriptionInputRefs . current [ activeMetricId ]
138+ if ( inputEl ) {
139+ inputEl . focus ( )
140+ const newCursorPos = lastOpenBracket + tag . length
141+ inputEl . setSelectionRange ( newCursorPos , newCursorPos )
142+ }
143+ } , 10 )
144+ }
145+
146+ const handleDescriptionChange = ( metricId : string , newValue : string , selectionStart ?: number ) => {
147+ updateMetric ( metricId , 'description' , newValue )
148+
149+ if ( selectionStart !== undefined ) {
150+ setCursorPosition ( selectionStart )
151+ setActiveMetricId ( metricId )
152+
153+ const shouldShowTags = checkTagTrigger ( newValue , selectionStart )
154+ setShowTags ( shouldShowTags . show )
155+
156+ if ( shouldShowTags . show ) {
157+ const textBeforeCursor = newValue . slice ( 0 , selectionStart )
158+ const lastOpenBracket = textBeforeCursor . lastIndexOf ( '<' )
159+ const tagContent = textBeforeCursor . slice ( lastOpenBracket + 1 )
160+ const dotIndex = tagContent . indexOf ( '.' )
161+ const sourceBlock = dotIndex > 0 ? tagContent . slice ( 0 , dotIndex ) : null
162+ setActiveSourceBlockId ( sourceBlock )
163+ }
164+ }
165+ }
166+
167+ const handleDrop = ( e : React . DragEvent , metricId : string ) => {
168+ e . preventDefault ( )
169+ setDragHighlight ( ( prev ) => ( { ...prev , [ metricId ] : false } ) )
170+ const input = descriptionInputRefs . current [ metricId ]
171+ input ?. focus ( )
172+
173+ if ( input ) {
174+ const metric = metrics . find ( ( m ) => m . id === metricId )
175+ const currentValue = metric ?. description || ''
176+ const dropPosition = input . selectionStart ?? currentValue . length
177+ const newValue = `${ currentValue . slice ( 0 , dropPosition ) } <${ currentValue . slice ( dropPosition ) } `
178+ updateMetric ( metricId , 'description' , newValue )
179+ setActiveMetricId ( metricId )
180+ setCursorPosition ( dropPosition + 1 )
181+ setShowTags ( true )
182+
183+ try {
184+ const data = JSON . parse ( e . dataTransfer . getData ( 'application/json' ) )
185+ if ( data ?. connectionData ?. sourceBlockId ) {
186+ setActiveSourceBlockId ( data . connectionData . sourceBlockId )
187+ }
188+ } catch { }
189+
190+ setTimeout ( ( ) => {
191+ const el = descriptionInputRefs . current [ metricId ]
192+ if ( el ) {
193+ el . selectionStart = dropPosition + 1
194+ el . selectionEnd = dropPosition + 1
195+ }
196+ } , 0 )
197+ }
198+ }
199+
200+ const handleDragOver = ( e : React . DragEvent , metricId : string ) => {
201+ e . preventDefault ( )
202+ e . dataTransfer . dropEffect = 'copy'
203+ setDragHighlight ( ( prev ) => ( { ...prev , [ metricId ] : true } ) )
204+ }
205+
206+ const handleDragLeave = ( e : React . DragEvent , metricId : string ) => {
207+ e . preventDefault ( )
208+ setDragHighlight ( ( prev ) => ( { ...prev , [ metricId ] : false } ) )
209+ }
210+
110211 const renderMetricHeader = ( metric : EvalMetric , index : number ) => (
111212 < div className = 'flex h-10 items-center justify-between rounded-t-lg border-b bg-card px-3' >
112213 < span className = 'font-medium text-sm' > Metric { index + 1 } </ span >
@@ -146,7 +247,6 @@ export function EvalInput({
146247 </ div >
147248 )
148249
149- // Main render
150250 return (
151251 < div className = 'space-y-2' >
152252 { metrics . map ( ( metric , index ) => (
@@ -172,13 +272,67 @@ export function EvalInput({
172272
173273 < div key = { `description-${ metric . id } ` } className = 'space-y-1' >
174274 < Label > Description</ Label >
175- < Input
176- value = { metric . description }
177- onChange = { ( e ) => updateMetric ( metric . id , 'description' , e . target . value ) }
178- placeholder = 'How accurate is the response?'
179- disabled = { isPreview || disabled }
180- className = 'placeholder:text-muted-foreground/50'
181- />
275+ < div className = 'relative' >
276+ < Textarea
277+ ref = { ( el ) => {
278+ if ( el ) descriptionInputRefs . current [ metric . id ] = el
279+ } }
280+ value = { metric . description }
281+ onChange = { ( e ) =>
282+ handleDescriptionChange (
283+ metric . id ,
284+ e . target . value ,
285+ e . target . selectionStart ?? undefined
286+ )
287+ }
288+ placeholder = 'How accurate is the response?'
289+ disabled = { isPreview || disabled }
290+ className = { cn (
291+ 'min-h-[80px] border border-input bg-white text-transparent caret-foreground placeholder:text-muted-foreground/50 dark:border-input/60 dark:bg-background' ,
292+ ( isConnecting || dragHighlight [ metric . id ] ) &&
293+ 'ring-2 ring-blue-500 ring-offset-2'
294+ ) }
295+ style = { {
296+ fontFamily : 'inherit' ,
297+ lineHeight : 'inherit' ,
298+ wordBreak : 'break-word' ,
299+ whiteSpace : 'pre-wrap' ,
300+ } }
301+ rows = { 3 }
302+ onDrop = { ( e ) => handleDrop ( e , metric . id ) }
303+ onDragOver = { ( e ) => handleDragOver ( e , metric . id ) }
304+ onDragLeave = { ( e ) => handleDragLeave ( e , metric . id ) }
305+ />
306+ < div
307+ ref = { ( el ) => {
308+ if ( el ) descriptionOverlayRefs . current [ metric . id ] = el
309+ } }
310+ className = 'pointer-events-none absolute inset-0 flex items-start overflow-auto bg-transparent px-3 py-2 text-sm'
311+ style = { {
312+ fontFamily : 'inherit' ,
313+ lineHeight : 'inherit' ,
314+ } }
315+ >
316+ < div className = 'w-full whitespace-pre-wrap break-words' >
317+ { formatDisplayText ( metric . description || '' , {
318+ accessiblePrefixes,
319+ highlightAll : ! accessiblePrefixes ,
320+ } ) }
321+ </ div >
322+ </ div >
323+ { showTags && activeMetricId === metric . id && (
324+ < TagDropdown
325+ visible = { showTags }
326+ onSelect = { handleTagSelect }
327+ blockId = { blockId }
328+ activeSourceBlockId = { activeSourceBlockId }
329+ inputValue = { metric . description || '' }
330+ cursorPosition = { cursorPosition }
331+ onClose = { ( ) => setShowTags ( false ) }
332+ className = 'absolute top-full left-0 z-50 mt-1'
333+ />
334+ ) }
335+ </ div >
182336 </ div >
183337
184338 < div key = { `range-${ metric . id } ` } className = 'grid grid-cols-2 gap-4' >
0 commit comments