Skip to content

Commit fd53247

Browse files
authored
fix(long-input): text overlay and resize UI (#329)
* improvement(long-input): added dynamic rows from config * fix(long-input): resize and overlay text misalignment
1 parent 73600fb commit fd53247

4 files changed

Lines changed: 101 additions & 8 deletions

File tree

sim/app/w/[id]/components/workflow-block/components/sub-block/components/long-input.tsx

Lines changed: 96 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { useRef, useState } from 'react'
1+
import { useEffect, useLayoutEffect, useRef, useState } from 'react'
2+
import { ChevronsUpDown } from 'lucide-react'
23
import { useReactFlow } from 'reactflow'
34
import { checkEnvVarTrigger, EnvVarDropdown } from '@/components/ui/env-var-dropdown'
45
import { formatDisplayText } from '@/components/ui/formatted-text'
@@ -17,14 +18,21 @@ interface LongInputProps {
1718
subBlockId: string
1819
isConnecting: boolean
1920
config: SubBlockConfig
21+
rows?: number
2022
}
2123

24+
// Constants
25+
const DEFAULT_ROWS = 4
26+
const ROW_HEIGHT_PX = 24
27+
const MIN_HEIGHT_PX = 80
28+
2229
export function LongInput({
2330
placeholder,
2431
blockId,
2532
subBlockId,
2633
isConnecting,
2734
config,
35+
rows,
2836
}: LongInputProps) {
2937
const [value, setValue] = useSubBlockValue(blockId, subBlockId)
3038
const [showEnvVars, setShowEnvVars] = useState(false)
@@ -34,10 +42,32 @@ export function LongInput({
3442
const textareaRef = useRef<HTMLTextAreaElement>(null)
3543
const overlayRef = useRef<HTMLDivElement>(null)
3644
const [activeSourceBlockId, setActiveSourceBlockId] = useState<string | null>(null)
45+
const containerRef = useRef<HTMLDivElement>(null)
46+
47+
// Calculate initial height based on rows prop with reasonable defaults
48+
const getInitialHeight = () => {
49+
// Use provided rows or default, then convert to pixels with a minimum
50+
const rowCount = rows || DEFAULT_ROWS
51+
return Math.max(rowCount * ROW_HEIGHT_PX, MIN_HEIGHT_PX)
52+
}
53+
54+
const [height, setHeight] = useState(getInitialHeight())
55+
const isResizing = useRef(false)
3756

3857
// Get ReactFlow instance for zoom control
3958
const reactFlowInstance = useReactFlow()
4059

60+
// Set initial height on first render
61+
useLayoutEffect(() => {
62+
const initialHeight = getInitialHeight()
63+
setHeight(initialHeight)
64+
65+
if (textareaRef.current && overlayRef.current) {
66+
textareaRef.current.style.height = `${initialHeight}px`
67+
overlayRef.current.style.height = `${initialHeight}px`
68+
}
69+
}, [rows])
70+
4171
// Handle input changes
4272
const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
4373
const newValue = e.target.value
@@ -63,6 +93,47 @@ export function LongInput({
6393
}
6494
}
6595

96+
// Ensure overlay updates when content changes
97+
useEffect(() => {
98+
if (textareaRef.current && overlayRef.current) {
99+
// Ensure scrolling is synchronized
100+
overlayRef.current.scrollTop = textareaRef.current.scrollTop
101+
overlayRef.current.scrollLeft = textareaRef.current.scrollLeft
102+
}
103+
}, [value])
104+
105+
// Handle resize functionality
106+
const startResize = (e: React.MouseEvent) => {
107+
e.preventDefault()
108+
isResizing.current = true
109+
110+
const startY = e.clientY
111+
const startHeight = height
112+
113+
const handleMouseMove = (moveEvent: MouseEvent) => {
114+
if (!isResizing.current) return
115+
116+
const deltaY = moveEvent.clientY - startY
117+
const newHeight = Math.max(MIN_HEIGHT_PX, startHeight + deltaY)
118+
119+
setHeight(newHeight)
120+
121+
if (textareaRef.current && overlayRef.current) {
122+
textareaRef.current.style.height = `${newHeight}px`
123+
overlayRef.current.style.height = `${newHeight}px`
124+
}
125+
}
126+
127+
const handleMouseUp = () => {
128+
isResizing.current = false
129+
document.removeEventListener('mousemove', handleMouseMove)
130+
document.removeEventListener('mouseup', handleMouseUp)
131+
}
132+
133+
document.addEventListener('mousemove', handleMouseMove)
134+
document.addEventListener('mouseup', handleMouseUp)
135+
}
136+
66137
// Drag and Drop handlers
67138
const handleDragOver = (e: React.DragEvent<HTMLTextAreaElement>) => {
68139
if (config?.connectionDroppable === false) return
@@ -131,9 +202,7 @@ export function LongInput({
131202
const { x: viewportX, y: viewportY } = reactFlowInstance.getViewport()
132203

133204
// Calculate zoom factor based on wheel delta
134-
// Use a smaller factor for smoother zooming that matches ReactFlow's native behavior
135205
const delta = e.deltaY > 0 ? 1 : -1
136-
// Using 0.98 instead of 0.95 makes the zoom much slower and more gradual
137206
const zoomFactor = Math.pow(0.96, delta)
138207

139208
// Calculate new zoom level with min/max constraints
@@ -169,16 +238,16 @@ export function LongInput({
169238
}
170239

171240
return (
172-
<div className="relative w-full">
241+
<div ref={containerRef} className="relative w-full" style={{ height: `${height}px` }}>
173242
<Textarea
174243
ref={textareaRef}
175244
className={cn(
176-
'w-full placeholder:text-muted-foreground/50 allow-scroll text-transparent caret-foreground break-words whitespace-pre-wrap box-border',
245+
'w-full min-h-full resize-none text-transparent caret-foreground placeholder:text-muted-foreground/50 allow-scroll',
177246
isConnecting &&
178247
config?.connectionDroppable !== false &&
179248
'focus-visible:ring-blue-500 ring-2 ring-blue-500 ring-offset-2'
180249
)}
181-
rows={4}
250+
rows={rows ?? DEFAULT_ROWS}
182251
placeholder={placeholder ?? ''}
183252
value={value?.toString() ?? ''}
184253
onChange={handleChange}
@@ -192,14 +261,33 @@ export function LongInput({
192261
setShowTags(false)
193262
setSearchTerm('')
194263
}}
264+
style={{
265+
fontFamily: 'inherit',
266+
lineHeight: 'inherit',
267+
height: `${height}px`,
268+
}}
195269
/>
196270
<div
197271
ref={overlayRef}
198-
className="absolute inset-0 pointer-events-none px-3 py-2 overflow-auto whitespace-pre-wrap break-words text-sm bg-transparent box-border"
199-
style={{ width: 'calc(100% - 2px)' }}
272+
className="absolute inset-0 pointer-events-none px-3 py-2 overflow-auto whitespace-pre-wrap break-words text-sm bg-transparent"
273+
style={{
274+
fontFamily: 'inherit',
275+
lineHeight: 'inherit',
276+
width: textareaRef.current ? `${textareaRef.current.clientWidth}px` : '100%',
277+
height: `${height}px`,
278+
}}
200279
>
201280
{formatDisplayText(value?.toString() ?? '', true)}
202281
</div>
282+
283+
{/* Custom resize handle */}
284+
<div
285+
className="absolute bottom-1 right-1 w-4 h-4 cursor-s-resize flex items-center justify-center bg-background rounded-sm"
286+
onMouseDown={startResize}
287+
>
288+
<ChevronsUpDown className="h-3 w-3 text-muted-foreground/70" />
289+
</div>
290+
203291
<EnvVarDropdown
204292
visible={showEnvVars}
205293
onSelect={setValue}

sim/app/w/[id]/components/workflow-block/components/sub-block/sub-block.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,7 @@ export function SubBlock({ blockId, config, isConnecting }: SubBlockProps) {
7070
subBlockId={config.id}
7171
placeholder={config.placeholder}
7272
isConnecting={isConnecting}
73+
rows={config.rows}
7374
config={config}
7475
/>
7576
)

sim/blocks/blocks/agent.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,13 +57,15 @@ export const AgentBlock: BlockConfig<AgentResponse> = {
5757
type: 'long-input',
5858
layout: 'full',
5959
placeholder: 'Enter system prompt...',
60+
rows: 5,
6061
},
6162
{
6263
id: 'context',
6364
title: 'User Prompt',
6465
type: 'long-input',
6566
layout: 'full',
6667
placeholder: 'Enter context or user message...',
68+
rows: 3,
6769
},
6870
{
6971
id: 'model',

sim/blocks/types.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ export interface SubBlockConfig {
126126
// Slider-specific properties
127127
step?: number
128128
integer?: boolean
129+
// Long input specific properties
130+
rows?: number
129131
}
130132

131133
// Main block definition

0 commit comments

Comments
 (0)