mirror of
https://github.com/simstudioai/sim.git
synced 2026-04-06 03:00:16 -04:00
fix(ux): minor ux changes (#1109)
* minor UX fixes * changed variable collapse * lint --------- Co-authored-by: Adam Gough <adamgough@Mac.attlocal.net>
This commit is contained in:
@@ -25,7 +25,7 @@ export function Console({ panelWidth }: ConsoleProps) {
|
||||
No console entries
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className='h-full' hideScrollbar={true}>
|
||||
<ScrollArea className='h-full' hideScrollbar={false}>
|
||||
<div className='space-y-3'>
|
||||
{filteredEntries.map((entry) => (
|
||||
<ConsoleEntry key={entry.id} entry={entry} consoleWidth={panelWidth} />
|
||||
|
||||
@@ -1,7 +1,16 @@
|
||||
'use client'
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { AlertTriangle, ChevronDown, Copy, MoreVertical, Plus, Trash } from 'lucide-react'
|
||||
import {
|
||||
AlertTriangle,
|
||||
ChevronDown,
|
||||
Copy,
|
||||
Maximize2,
|
||||
Minimize2,
|
||||
MoreVertical,
|
||||
Plus,
|
||||
Trash,
|
||||
} from 'lucide-react'
|
||||
import { highlight, languages } from 'prismjs'
|
||||
import 'prismjs/components/prism-javascript'
|
||||
import 'prismjs/themes/prism.css'
|
||||
@@ -52,6 +61,16 @@ export function Variables() {
|
||||
// Track which variables are currently being edited
|
||||
const [_activeEditors, setActiveEditors] = useState<Record<string, boolean>>({})
|
||||
|
||||
// Collapsed state per variable
|
||||
const [collapsedById, setCollapsedById] = useState<Record<string, boolean>>({})
|
||||
|
||||
const toggleCollapsed = (variableId: string) => {
|
||||
setCollapsedById((prev) => ({
|
||||
...prev,
|
||||
[variableId]: !prev[variableId],
|
||||
}))
|
||||
}
|
||||
|
||||
// Handle variable name change with validation
|
||||
const handleVariableNameChange = (variableId: string, newName: string) => {
|
||||
const validatedName = validateName(newName)
|
||||
@@ -220,7 +239,7 @@ export function Variables() {
|
||||
</Button>
|
||||
</div>
|
||||
) : (
|
||||
<ScrollArea className='h-full' hideScrollbar={true}>
|
||||
<ScrollArea className='h-full' hideScrollbar={false}>
|
||||
<div className='space-y-4'>
|
||||
{workflowVariables.map((variable) => (
|
||||
<div key={variable.id} className='space-y-2'>
|
||||
@@ -298,6 +317,17 @@ export function Variables() {
|
||||
align='end'
|
||||
className='min-w-32 rounded-lg border-[#E5E5E5] bg-[#FFFFFF] shadow-xs dark:border-[#414141] dark:bg-[var(--surface-elevated)]'
|
||||
>
|
||||
<DropdownMenuItem
|
||||
onClick={() => toggleCollapsed(variable.id)}
|
||||
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
|
||||
>
|
||||
{(collapsedById[variable.id] ?? false) ? (
|
||||
<Maximize2 className='mr-2 h-4 w-4 text-muted-foreground' />
|
||||
) : (
|
||||
<Minimize2 className='mr-2 h-4 w-4 text-muted-foreground' />
|
||||
)}
|
||||
{(collapsedById[variable.id] ?? false) ? 'Expand' : 'Collapse'}
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => collaborativeDuplicateVariable(variable.id)}
|
||||
className='cursor-pointer rounded-md px-3 py-2 font-[380] text-card-foreground text-sm hover:bg-secondary/50 focus:bg-secondary/50'
|
||||
@@ -317,71 +347,75 @@ export function Variables() {
|
||||
</div>
|
||||
|
||||
{/* Value area */}
|
||||
<div className='relative rounded-lg bg-secondary/50'>
|
||||
{/* Validation indicator */}
|
||||
{variable.value !== '' && getValidationStatus(variable) && (
|
||||
<div className='absolute top-2 right-2 z-10'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='cursor-help'>
|
||||
<AlertTriangle className='h-3 w-3 text-muted-foreground' />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='bottom' className='max-w-xs'>
|
||||
<p>{getValidationStatus(variable)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
{!(collapsedById[variable.id] ?? false) && (
|
||||
<div className='relative rounded-lg bg-secondary/50'>
|
||||
{/* Validation indicator */}
|
||||
{variable.value !== '' && getValidationStatus(variable) && (
|
||||
<div className='absolute top-2 right-2 z-10'>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div className='cursor-help'>
|
||||
<AlertTriangle className='h-3 w-3 text-muted-foreground' />
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side='bottom' className='max-w-xs'>
|
||||
<p>{getValidationStatus(variable)}</p>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Editor */}
|
||||
<div className='relative overflow-hidden'>
|
||||
<div
|
||||
className='relative min-h-[36px] w-full max-w-full px-3 py-2 font-normal text-sm'
|
||||
ref={(el) => {
|
||||
editorRefs.current[variable.id] = el
|
||||
}}
|
||||
style={{ maxWidth: '100%' }}
|
||||
>
|
||||
{variable.value === '' && (
|
||||
<div className='pointer-events-none absolute inset-0 flex select-none items-start justify-start px-3 py-2 font-[380] text-muted-foreground text-sm leading-normal'>
|
||||
<div style={{ lineHeight: '20px' }}>{getPlaceholder(variable.type)}</div>
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
key={`editor-${variable.id}-${variable.type}`}
|
||||
value={formatValue(variable)}
|
||||
onValueChange={handleEditorChange.bind(null, variable)}
|
||||
onBlur={() => handleEditorBlur(variable.id)}
|
||||
onFocus={() => handleEditorFocus(variable.id)}
|
||||
highlight={(code) =>
|
||||
// Only apply syntax highlighting for non-basic text types
|
||||
variable.type === 'plain' || variable.type === 'string'
|
||||
? code
|
||||
: highlight(
|
||||
code,
|
||||
languages[getEditorLanguage(variable.type)],
|
||||
getEditorLanguage(variable.type)
|
||||
)
|
||||
}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: '20px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
overflowWrap: 'break-word',
|
||||
minHeight: '20px',
|
||||
overflow: 'hidden',
|
||||
{/* Editor */}
|
||||
<div className='relative overflow-hidden'>
|
||||
<div
|
||||
className='relative min-h-[36px] w-full max-w-full px-3 py-2 font-normal text-sm'
|
||||
ref={(el) => {
|
||||
editorRefs.current[variable.id] = el
|
||||
}}
|
||||
className='[&>pre]:!max-w-full [&>pre]:!overflow-hidden [&>pre]:!whitespace-pre-wrap [&>pre]:!break-all [&>pre]:!overflow-wrap-break-word [&>textarea]:!max-w-full [&>textarea]:!overflow-hidden [&>textarea]:!whitespace-pre-wrap [&>textarea]:!break-all [&>textarea]:!overflow-wrap-break-word font-[380] text-foreground text-sm leading-normal focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full max-w-full whitespace-pre-wrap break-all overflow-wrap-break-word overflow-hidden font-[380] text-foreground'
|
||||
/>
|
||||
style={{ maxWidth: '100%' }}
|
||||
>
|
||||
{variable.value === '' && (
|
||||
<div className='pointer-events-none absolute inset-0 flex select-none items-start justify-start px-3 py-2 font-[380] text-muted-foreground text-sm leading-normal'>
|
||||
<div style={{ lineHeight: '20px' }}>
|
||||
{getPlaceholder(variable.type)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<Editor
|
||||
key={`editor-${variable.id}-${variable.type}`}
|
||||
value={formatValue(variable)}
|
||||
onValueChange={handleEditorChange.bind(null, variable)}
|
||||
onBlur={() => handleEditorBlur(variable.id)}
|
||||
onFocus={() => handleEditorFocus(variable.id)}
|
||||
highlight={(code) =>
|
||||
// Only apply syntax highlighting for non-basic text types
|
||||
variable.type === 'plain' || variable.type === 'string'
|
||||
? code
|
||||
: highlight(
|
||||
code,
|
||||
languages[getEditorLanguage(variable.type)],
|
||||
getEditorLanguage(variable.type)
|
||||
)
|
||||
}
|
||||
padding={0}
|
||||
style={{
|
||||
fontFamily: 'inherit',
|
||||
lineHeight: '20px',
|
||||
width: '100%',
|
||||
maxWidth: '100%',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-all',
|
||||
overflowWrap: 'break-word',
|
||||
minHeight: '20px',
|
||||
overflow: 'hidden',
|
||||
}}
|
||||
className='[&>pre]:!max-w-full [&>pre]:!overflow-hidden [&>pre]:!whitespace-pre-wrap [&>pre]:!break-all [&>pre]:!overflow-wrap-break-word [&>textarea]:!max-w-full [&>textarea]:!overflow-hidden [&>textarea]:!whitespace-pre-wrap [&>textarea]:!break-all [&>textarea]:!overflow-wrap-break-word font-[380] text-foreground text-sm leading-normal focus:outline-none'
|
||||
textareaClassName='focus:outline-none focus:ring-0 bg-transparent resize-none w-full max-w-full whitespace-pre-wrap break-all overflow-wrap-break-word overflow-hidden font-[380] text-foreground'
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
|
||||
|
||||
@@ -667,6 +667,11 @@ const WorkflowContent = React.memo(() => {
|
||||
y: position.y - containerInfo.loopPosition.y,
|
||||
}
|
||||
|
||||
// Capture existing child blocks before adding the new one
|
||||
const existingChildBlocks = Object.values(blocks).filter(
|
||||
(b) => b.data?.parentId === containerInfo.loopId
|
||||
)
|
||||
|
||||
// Add block with parent info
|
||||
addBlock(id, data.type, name, relativePosition, {
|
||||
parentId: containerInfo.loopId,
|
||||
@@ -680,12 +685,35 @@ const WorkflowContent = React.memo(() => {
|
||||
// Auto-connect logic for blocks inside containers
|
||||
const isAutoConnectEnabled = useGeneralStore.getState().isAutoConnectEnabled
|
||||
if (isAutoConnectEnabled && data.type !== 'starter') {
|
||||
// First priority: Connect to the container's start node
|
||||
const containerNode = getNodes().find((n) => n.id === containerInfo.loopId)
|
||||
const containerType = containerNode?.type
|
||||
if (existingChildBlocks.length > 0) {
|
||||
// Connect to the nearest existing child block within the container
|
||||
const closestBlock = existingChildBlocks
|
||||
.map((b) => ({
|
||||
block: b,
|
||||
distance: Math.sqrt(
|
||||
(b.position.x - relativePosition.x) ** 2 +
|
||||
(b.position.y - relativePosition.y) ** 2
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)[0]?.block
|
||||
|
||||
if (containerType === 'subflowNode') {
|
||||
// Connect from the container's start node to the new block
|
||||
if (closestBlock) {
|
||||
const sourceHandle = determineSourceHandle({
|
||||
id: closestBlock.id,
|
||||
type: closestBlock.type,
|
||||
})
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
source: closestBlock.id,
|
||||
target: id,
|
||||
sourceHandle,
|
||||
targetHandle: 'target',
|
||||
type: 'workflowEdge',
|
||||
})
|
||||
}
|
||||
} else {
|
||||
// No existing children: connect from the container's start handle
|
||||
const containerNode = getNodes().find((n) => n.id === containerInfo.loopId)
|
||||
const startSourceHandle =
|
||||
(containerNode?.data as any)?.kind === 'loop'
|
||||
? 'loop-start-source'
|
||||
@@ -699,45 +727,6 @@ const WorkflowContent = React.memo(() => {
|
||||
targetHandle: 'target',
|
||||
type: 'workflowEdge',
|
||||
})
|
||||
} else {
|
||||
// Fallback: Try to find other nodes in the container to connect to
|
||||
const containerNodes = getNodes().filter((n) => n.parentId === containerInfo.loopId)
|
||||
|
||||
if (containerNodes.length > 0) {
|
||||
// Connect to the closest node in the container
|
||||
const closestNode = containerNodes
|
||||
.map((n) => ({
|
||||
id: n.id,
|
||||
distance: Math.sqrt(
|
||||
(n.position.x - relativePosition.x) ** 2 +
|
||||
(n.position.y - relativePosition.y) ** 2
|
||||
),
|
||||
}))
|
||||
.sort((a, b) => a.distance - b.distance)[0]
|
||||
|
||||
if (closestNode) {
|
||||
// Get appropriate source handle
|
||||
const sourceNode = getNodes().find((n) => n.id === closestNode.id)
|
||||
const sourceType = sourceNode?.data?.type
|
||||
|
||||
// Default source handle
|
||||
let sourceHandle = 'source'
|
||||
|
||||
// For condition blocks, use the condition-true handle
|
||||
if (sourceType === 'condition') {
|
||||
sourceHandle = 'condition-true'
|
||||
}
|
||||
|
||||
addEdge({
|
||||
id: crypto.randomUUID(),
|
||||
source: closestNode.id,
|
||||
target: id,
|
||||
sourceHandle,
|
||||
targetHandle: 'target',
|
||||
type: 'workflowEdge',
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type React from 'react'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { ChevronRight } from 'lucide-react'
|
||||
import { BlockPathCalculator } from '@/lib/block-path-calculator'
|
||||
import { extractFieldsFromSchema, parseResponseFormatSafely } from '@/lib/response-format'
|
||||
@@ -283,6 +283,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
onClose,
|
||||
style,
|
||||
}) => {
|
||||
const containerRef = useRef<HTMLDivElement>(null)
|
||||
const [selectedIndex, setSelectedIndex] = useState(0)
|
||||
const [hoveredNested, setHoveredNested] = useState<{ tag: string; index: number } | null>(null)
|
||||
const [inSubmenu, setInSubmenu] = useState(false)
|
||||
@@ -949,6 +950,28 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
}
|
||||
}, [orderedTags.length, selectedIndex])
|
||||
|
||||
// Close on outside click/touch when dropdown is visible
|
||||
useEffect(() => {
|
||||
if (!visible) return
|
||||
|
||||
const handlePointerDown = (e: MouseEvent | TouchEvent) => {
|
||||
const el = containerRef.current
|
||||
if (!el) return
|
||||
const target = e.target as Node
|
||||
if (!el.contains(target)) {
|
||||
onClose?.()
|
||||
}
|
||||
}
|
||||
|
||||
// Use capture phase to detect before child handlers potentially stop propagation
|
||||
document.addEventListener('mousedown', handlePointerDown, true)
|
||||
document.addEventListener('touchstart', handlePointerDown, true)
|
||||
return () => {
|
||||
document.removeEventListener('mousedown', handlePointerDown, true)
|
||||
document.removeEventListener('touchstart', handlePointerDown, true)
|
||||
}
|
||||
}, [visible, onClose])
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
const handleKeyboardEvent = (e: KeyboardEvent) => {
|
||||
@@ -1173,6 +1196,7 @@ export const TagDropdown: React.FC<TagDropdownProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn(
|
||||
'absolute z-[9999] mt-1 w-full overflow-visible rounded-md border bg-popover shadow-md',
|
||||
className
|
||||
|
||||
Reference in New Issue
Block a user