diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx index c86e934231..ea922b3baf 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/combobox/combobox.tsx @@ -1,6 +1,7 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { isEqual } from 'lodash' import { useReactFlow } from 'reactflow' +import { useStoreWithEqualityFn } from 'zustand/traditional' import { Combobox, type ComboboxOption } from '@/components/emcn/components' import { cn } from '@/lib/core/utils/cn' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' @@ -102,7 +103,8 @@ export const ComboBox = memo(function ComboBox({ [blockConfig?.subBlocks] ) const canonicalModeOverrides = blockState?.data?.canonicalModes - const dependencyValues = useSubBlockStore( + const dependencyValues = useStoreWithEqualityFn( + useSubBlockStore, useCallback( (state) => { if (dependsOnFields.length === 0 || !activeWorkflowId) return [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx index 51aa596a5b..d8d3ec00ec 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/dropdown/dropdown.tsx @@ -1,5 +1,6 @@ import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react' import { isEqual } from 'lodash' +import { useStoreWithEqualityFn } from 'zustand/traditional' import { Badge } from '@/components/emcn' import { Combobox, type ComboboxOption } from '@/components/emcn/components' import { buildCanonicalIndex, resolveDependencyValue } from '@/lib/workflows/subblocks/visibility' @@ -100,7 +101,8 @@ export const Dropdown = memo(function Dropdown({ [blockConfig?.subBlocks] ) const canonicalModeOverrides = blockState?.data?.canonicalModes - const dependencyValues = useSubBlockStore( + const dependencyValues = useStoreWithEqualityFn( + useSubBlockStore, useCallback( (state) => { if (dependsOnFields.length === 0 || !activeWorkflowId) return [] diff --git a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx index 6ada0e47cb..30b3fa2e94 100644 --- a/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx +++ b/apps/sim/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/messages-input/messages-input.tsx @@ -1,4 +1,12 @@ -import { useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react' +import { + useCallback, + useEffect, + useImperativeHandle, + useLayoutEffect, + useMemo, + useRef, + useState, +} from 'react' import { isEqual } from 'lodash' import { ChevronDown, ChevronsUpDown, ChevronUp, Plus } from 'lucide-react' import { Button, Popover, PopoverContent, PopoverItem, PopoverTrigger } from '@/components/emcn' @@ -382,93 +390,138 @@ export function MessagesInput({ textareaRefs.current[fieldId]?.focus() }, []) - const autoResizeTextarea = useCallback((fieldId: string) => { + const syncOverlay = useCallback((fieldId: string) => { const textarea = textareaRefs.current[fieldId] - if (!textarea) return const overlay = overlayRefs.current[fieldId] + if (!textarea || !overlay) return - // If user has manually resized, respect their chosen height and only sync overlay. - if (userResizedRef.current[fieldId]) { - const currentHeight = - textarea.offsetHeight || Number.parseFloat(textarea.style.height) || MIN_TEXTAREA_HEIGHT_PX - const clampedHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, currentHeight) - textarea.style.height = `${clampedHeight}px` + overlay.style.width = `${textarea.clientWidth}px` + overlay.scrollTop = textarea.scrollTop + overlay.scrollLeft = textarea.scrollLeft + }, []) + + const autoResizeTextarea = useCallback( + (fieldId: string) => { + const textarea = textareaRefs.current[fieldId] + const overlay = overlayRefs.current[fieldId] + if (!textarea) return + + if (!textarea.value.trim()) { + userResizedRef.current[fieldId] = false + } + + if (userResizedRef.current[fieldId]) { + if (overlay) { + overlay.style.height = `${textarea.offsetHeight}px` + } + syncOverlay(fieldId) + return + } + + textarea.style.height = 'auto' + const scrollHeight = textarea.scrollHeight + const height = Math.min( + MAX_TEXTAREA_HEIGHT_PX, + Math.max(MIN_TEXTAREA_HEIGHT_PX, scrollHeight) + ) + + textarea.style.height = `${height}px` if (overlay) { - overlay.style.height = `${clampedHeight}px` + overlay.style.height = `${height}px` } - return - } - textarea.style.height = 'auto' - const naturalHeight = textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX - const nextHeight = Math.min( - MAX_TEXTAREA_HEIGHT_PX, - Math.max(MIN_TEXTAREA_HEIGHT_PX, naturalHeight) - ) - textarea.style.height = `${nextHeight}px` + syncOverlay(fieldId) + }, + [syncOverlay] + ) - if (overlay) { - overlay.style.height = `${nextHeight}px` - } - }, []) + const handleResizeStart = useCallback( + (fieldId: string, e: React.MouseEvent) => { + e.preventDefault() + e.stopPropagation() - const handleResizeStart = useCallback((fieldId: string, e: React.MouseEvent) => { - e.preventDefault() - e.stopPropagation() + const textarea = textareaRefs.current[fieldId] + if (!textarea) return - const textarea = textareaRefs.current[fieldId] - if (!textarea) return + const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX - const startHeight = textarea.offsetHeight || textarea.scrollHeight || MIN_TEXTAREA_HEIGHT_PX + isResizingRef.current = true + resizeStateRef.current = { + fieldId, + startY: e.clientY, + startHeight, + } - isResizingRef.current = true - resizeStateRef.current = { - fieldId, - startY: e.clientY, - startHeight, - } + const handleMouseMove = (moveEvent: MouseEvent) => { + if (!isResizingRef.current || !resizeStateRef.current) return - const handleMouseMove = (moveEvent: MouseEvent) => { - if (!isResizingRef.current || !resizeStateRef.current) return + const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current + const deltaY = moveEvent.clientY - startY + const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY) - const { fieldId: activeFieldId, startY, startHeight } = resizeStateRef.current - const deltaY = moveEvent.clientY - startY - const nextHeight = Math.max(MIN_TEXTAREA_HEIGHT_PX, startHeight + deltaY) + const activeTextarea = textareaRefs.current[activeFieldId] + const overlay = overlayRefs.current[activeFieldId] - const activeTextarea = textareaRefs.current[activeFieldId] - if (activeTextarea) { - activeTextarea.style.height = `${nextHeight}px` - } + if (activeTextarea) { + activeTextarea.style.height = `${nextHeight}px` + } - const overlay = overlayRefs.current[activeFieldId] - if (overlay) { - overlay.style.height = `${nextHeight}px` + if (overlay) { + overlay.style.height = `${nextHeight}px` + if (activeTextarea) { + overlay.scrollTop = activeTextarea.scrollTop + overlay.scrollLeft = activeTextarea.scrollLeft + } + } } - } - const handleMouseUp = () => { - if (resizeStateRef.current) { - const { fieldId: activeFieldId } = resizeStateRef.current - userResizedRef.current[activeFieldId] = true - } + const handleMouseUp = () => { + if (resizeStateRef.current) { + const { fieldId: activeFieldId } = resizeStateRef.current + userResizedRef.current[activeFieldId] = true + syncOverlay(activeFieldId) + } - isResizingRef.current = false - resizeStateRef.current = null - document.removeEventListener('mousemove', handleMouseMove) - document.removeEventListener('mouseup', handleMouseUp) - } + isResizingRef.current = false + resizeStateRef.current = null + document.removeEventListener('mousemove', handleMouseMove) + document.removeEventListener('mouseup', handleMouseUp) + } - document.addEventListener('mousemove', handleMouseMove) - document.addEventListener('mouseup', handleMouseUp) - }, []) + document.addEventListener('mousemove', handleMouseMove) + document.addEventListener('mouseup', handleMouseUp) + }, + [syncOverlay] + ) - useEffect(() => { + useLayoutEffect(() => { currentMessages.forEach((_, index) => { - const fieldId = `message-${index}` - autoResizeTextarea(fieldId) + autoResizeTextarea(`message-${index}`) }) }, [currentMessages, autoResizeTextarea]) + useEffect(() => { + const observers: ResizeObserver[] = [] + + for (let i = 0; i < currentMessages.length; i++) { + const fieldId = `message-${i}` + const textarea = textareaRefs.current[fieldId] + const overlay = overlayRefs.current[fieldId] + + if (textarea && overlay) { + const observer = new ResizeObserver(() => { + overlay.style.width = `${textarea.clientWidth}px` + }) + observer.observe(textarea) + observers.push(observer) + } + } + + return () => { + observers.forEach((observer) => observer.disconnect()) + } + }, [currentMessages.length]) + return (
{currentMessages.map((message, index) => ( @@ -621,19 +674,15 @@ export function MessagesInput({
{/* Content Input with overlay for variable highlighting */} -
+