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:
Adam Gough
2025-08-23 15:50:40 -07:00
committed by GitHub
parent 730164abee
commit 79dd1ccb9f
4 changed files with 156 additions and 109 deletions

View File

@@ -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} />

View File

@@ -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>
))}

View File

@@ -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 {

View File

@@ -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