Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 []
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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 []
Expand Down
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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<HTMLDivElement>) => {
e.preventDefault()
e.stopPropagation()

const handleResizeStart = useCallback((fieldId: string, e: React.MouseEvent<HTMLDivElement>) => {
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 (
<div className='flex w-full flex-col gap-[10px]'>
{currentMessages.map((message, index) => (
Expand Down Expand Up @@ -621,19 +674,15 @@ export function MessagesInput({
</div>

{/* Content Input with overlay for variable highlighting */}
<div className='relative w-full'>
<div className='relative w-full overflow-hidden'>
<textarea
ref={(el) => {
textareaRefs.current[fieldId] = el
}}
className='allow-scroll box-border min-h-[80px] w-full resize-none whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[8px] font-[inherit] font-medium text-sm text-transparent leading-[inherit] caret-[var(--text-primary)] outline-none placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed'
rows={3}
className='relative z-[2] m-0 box-border h-auto min-h-[80px] w-full resize-none overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-none bg-transparent px-[8px] py-[8px] font-medium font-sans text-sm text-transparent leading-[1.5] caret-[var(--text-primary)] outline-none [-ms-overflow-style:none] [scrollbar-width:none] placeholder:text-[var(--text-muted)] focus:outline-none focus-visible:outline-none disabled:cursor-not-allowed [&::-webkit-scrollbar]:hidden'
placeholder='Enter message content...'
value={message.content}
onChange={(e) => {
fieldHandlers.onChange(e)
autoResizeTextarea(fieldId)
}}
onChange={fieldHandlers.onChange}
onKeyDown={(e) => {
if (e.key === 'Tab' && !isPreview && !disabled) {
e.preventDefault()
Expand Down Expand Up @@ -670,12 +719,13 @@ export function MessagesInput({
ref={(el) => {
overlayRefs.current[fieldId] = el
}}
className='scrollbar-none pointer-events-none absolute top-0 left-0 box-border w-full overflow-auto whitespace-pre-wrap break-words border-none bg-transparent px-[8px] pt-[8px] font-[inherit] font-medium text-[var(--text-primary)] text-sm leading-[inherit]'
className='pointer-events-none absolute top-0 left-0 z-[1] m-0 box-border w-full overflow-y-auto overflow-x-hidden whitespace-pre-wrap break-words border-none bg-transparent px-[8px] py-[8px] font-medium font-sans text-[var(--text-primary)] text-sm leading-[1.5] [-ms-overflow-style:none] [scrollbar-width:none] [&::-webkit-scrollbar]:hidden'
>
{formatDisplayText(message.content, {
accessiblePrefixes,
highlightAll: !accessiblePrefixes,
})}
{message.content.endsWith('\n') && '\u200B'}
</div>

{/* Env var dropdown for this message */}
Expand Down Expand Up @@ -705,7 +755,7 @@ export function MessagesInput({

{!isPreview && !disabled && (
<div
className='absolute right-1 bottom-1 flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
className='absolute right-1 bottom-1 z-[3] flex h-4 w-4 cursor-ns-resize items-center justify-center rounded-[4px] border border-[var(--border-1)] bg-[var(--surface-5)] dark:bg-[var(--surface-5)]'
onMouseDown={(e) => handleResizeStart(fieldId, e)}
onDragStart={(e) => {
e.preventDefault()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import { useCallback, useMemo } from 'react'
import { isEqual } from 'lodash'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import {
buildCanonicalIndex,
isNonEmptyValue,
Expand Down Expand Up @@ -151,7 +152,7 @@ export function useDependsOnGate(

// Get values for all dependency fields (both all and any)
// Use isEqual to prevent re-renders when dependency values haven't actually changed
const dependencyValuesMap = useSubBlockStore(dependencySelector, isEqual)
const dependencyValuesMap = useStoreWithEqualityFn(useSubBlockStore, dependencySelector, isEqual)

const depsSatisfied = useMemo(() => {
// Check all fields (AND logic) - all must be satisfied
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { useCallback, useEffect, useRef } from 'react'
import { createLogger } from '@sim/logger'
import { isEqual } from 'lodash'
import { useShallow } from 'zustand/react/shallow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
import { getProviderFromModel } from '@/providers/utils'
import { useWorkflowDiffStore } from '@/stores/workflow-diff/store'
Expand Down Expand Up @@ -58,7 +59,8 @@ export function useSubBlockValue<T = any>(
const streamingValueRef = useRef<T | null>(null)
const wasStreamingRef = useRef<boolean>(false)

const storeValue = useSubBlockStore(
const storeValue = useStoreWithEqualityFn(
useSubBlockStore,
useCallback(
(state) => {
// If the active workflow ID isn't available yet, return undefined so we can fall back to initialValue
Expand Down Expand Up @@ -92,7 +94,8 @@ export function useSubBlockValue<T = any>(

// Always call this hook unconditionally - don't wrap it in a condition
// Optimized: only re-render if model value actually changes
const modelSubBlockValue = useSubBlockStore(
const modelSubBlockValue = useStoreWithEqualityFn(
useSubBlockStore,
useCallback((state) => (blockId ? state.getValue(blockId, 'model') : null), [blockId]),
(a, b) => a === b
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { isEqual } from 'lodash'
import { BookOpen, Check, ChevronDown, ChevronUp, Pencil } from 'lucide-react'
import { useShallow } from 'zustand/react/shallow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { Button, Tooltip } from '@/components/emcn'
import {
buildCanonicalIndex,
Expand Down Expand Up @@ -99,7 +100,8 @@ export function Editor() {
currentWorkflow.isSnapshotView
)

const blockSubBlockValues = useSubBlockStore(
const blockSubBlockValues = useStoreWithEqualityFn(
useSubBlockStore,
useCallback(
(state) => {
if (!activeWorkflowId || !currentBlockId) return EMPTY_SUBBLOCK_VALUES
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { createLogger } from '@sim/logger'
import { isEqual } from 'lodash'
import { useParams } from 'next/navigation'
import { Handle, type NodeProps, Position, useUpdateNodeInternals } from 'reactflow'
import { useStoreWithEqualityFn } from 'zustand/traditional'
import { Badge, Tooltip } from '@/components/emcn'
import { cn } from '@/lib/core/utils/cn'
import { getBaseUrl } from '@/lib/core/utils/urls'
Expand Down Expand Up @@ -526,7 +527,8 @@ const SubBlockRow = memo(function SubBlockRow({
* Subscribe only to variables for this workflow to avoid re-renders from other workflows.
* Uses isEqual for deep comparison since Object.fromEntries creates a new object each time.
*/
const workflowVariables = useVariablesStore(
const workflowVariables = useStoreWithEqualityFn(
useVariablesStore,
useCallback(
(state) => {
if (!workflowId) return {}
Expand Down Expand Up @@ -729,7 +731,8 @@ export const WorkflowBlock = memo(function WorkflowBlock({
const isStarterBlock = type === 'starter'
const isWebhookTriggerBlock = type === 'webhook' || type === 'generic_webhook'

const blockSubBlockValues = useSubBlockStore(
const blockSubBlockValues = useStoreWithEqualityFn(
useSubBlockStore,
useCallback(
(state) => {
if (!activeWorkflowId) return EMPTY_SUBBLOCK_VALUES
Expand Down
Loading