From 16458c7e5dc7fe2c6033660b4ccab20d09093f33 Mon Sep 17 00:00:00 2001 From: Emir Karabeg Date: Mon, 24 Mar 2025 17:21:50 -0700 Subject: [PATCH] improvement(ui/ux): connections and styling --- .../components/control-bar/control-bar.tsx | 29 ++--- .../toolbar-block/toolbar-block.tsx | 2 +- sim/app/w/[id]/components/toolbar/toolbar.tsx | 4 +- .../connection-blocks/connection-blocks.tsx | 18 ++- .../sub-block/components/dropdown.tsx | 2 +- sim/app/w/[id]/hooks/use-block-connections.ts | 108 +++++++++++++++++- sim/app/w/[id]/layout.tsx | 2 +- .../components/filters/components/level.tsx | 5 +- .../filters/components/timeline.tsx | 5 +- .../filters/components/workflow.tsx | 12 +- sim/app/w/logs/components/filters/filters.tsx | 2 +- .../components/toolbar/toolbar.tsx | 8 +- sim/blocks/blocks/github.ts | 2 +- sim/components/ui/select.tsx | 18 ++- 14 files changed, 168 insertions(+), 49 deletions(-) diff --git a/sim/app/w/[id]/components/control-bar/control-bar.tsx b/sim/app/w/[id]/components/control-bar/control-bar.tsx index 56547923cb..a1555c240b 100644 --- a/sim/app/w/[id]/components/control-bar/control-bar.tsx +++ b/sim/app/w/[id]/components/control-bar/control-bar.tsx @@ -84,7 +84,6 @@ export function ControlBar() { const [runCount, setRunCount] = useState(1) const [completedRuns, setCompletedRuns] = useState(0) const [isMultiRunning, setIsMultiRunning] = useState(false) - const [showRunProgress, setShowRunProgress] = useState(false) // Get notifications for current workflow const workflowNotifications = activeWorkflowId @@ -305,7 +304,6 @@ export function ControlBar() { // Reset state for a new batch of runs setCompletedRuns(0) setIsMultiRunning(true) - setShowRunProgress(runCount > 1) try { // Run the workflow multiple times sequentially @@ -331,10 +329,6 @@ export function ControlBar() { addNotification('error', 'Failed to complete all workflow runs', activeWorkflowId) } finally { setIsMultiRunning(false) - // Keep progress visible for a moment after completion - if (runCount > 1) { - setTimeout(() => setShowRunProgress(false), 2000) - } } } @@ -583,15 +577,6 @@ export function ControlBar() { */ const renderRunButton = () => (
- {showRunProgress && ( -
- -

- {completedRuns}/{runCount} runs -

-
- )} -
{/* Main Run Button */} - + {RUN_COUNT_OPTIONS.map((count) => ( setRunCount(count)} - className={cn('justify-center', runCount === count && 'bg-muted')} + className={cn('justify-center cursor-pointer', runCount === count && 'bg-muted')} > {count} diff --git a/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx b/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx index 3069602351..4ed3c030d0 100644 --- a/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx +++ b/sim/app/w/[id]/components/toolbar/components/toolbar-block/toolbar-block.tsx @@ -29,7 +29,7 @@ export function ToolbarBlock({ config }: ToolbarBlockProps) { draggable onDragStart={handleDragStart} onClick={handleClick} - className="group flex items-center gap-3 rounded-lg border bg-card p-4 shadow-sm transition-colors hover:bg-accent/50 cursor-pointer active:cursor-grabbing" + className="group flex items-center gap-3 rounded-lg border bg-card p-3.5 shadow-sm transition-colors hover:bg-accent/50 cursor-pointer active:cursor-grabbing" >
+
setSearchQuery(e.target.value)} autoComplete="off" diff --git a/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx b/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx index 90e19f3d35..19505e20c0 100644 --- a/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/connection-blocks/connection-blocks.tsx @@ -69,9 +69,23 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP })) } + // Group connections by their ID for better organization + const connectionsByBlock = incomingConnections.reduce( + (acc, connection) => { + acc[connection.id] = connection + return acc + }, + {} as Record + ) + + // Sort connections by name to make it easier to find blocks + const sortedConnections = Object.values(connectionsByBlock).sort((a, b) => + a.name.localeCompare(b.name) + ) + return ( -
- {incomingConnections.map((connection) => ( +
+ {sortedConnections.map((connection) => (
{Array.isArray(connection.outputType) ? ( // Handle array of field names diff --git a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx index 073256746a..ebd8deec20 100644 --- a/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx +++ b/sim/app/w/[id]/components/workflow-block/components/sub-block/components/dropdown.tsx @@ -44,7 +44,7 @@ export function Dropdown({ options, defaultValue, blockId, subBlockId }: Dropdow - + {options.map((option) => ( {getOptionLabel(option)} diff --git a/sim/app/w/[id]/hooks/use-block-connections.ts b/sim/app/w/[id]/hooks/use-block-connections.ts index fbc154cf82..df1b950b0e 100644 --- a/sim/app/w/[id]/hooks/use-block-connections.ts +++ b/sim/app/w/[id]/hooks/use-block-connections.ts @@ -53,6 +53,63 @@ function extractFieldsFromSchema(schema: any): Field[] { })) } +/** + * Finds all blocks along paths leading to the target block + * This is a reverse traversal from the target node to find all ancestors + * along connected paths + * @param edges - List of all edges in the graph + * @param targetNodeId - ID of the target block we're finding connections for + * @returns Array of unique ancestor node IDs + */ +function findAllPathNodes(edges: any[], targetNodeId: string): string[] { + // We'll use a reverse topological sort approach by tracking "distance" from target + const nodeDistances = new Map() + const visited = new Set() + const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance] + const pathNodes = new Set() + + // Build a reverse adjacency list for faster traversal + const reverseAdjList: Record = {} + for (const edge of edges) { + if (!reverseAdjList[edge.target]) { + reverseAdjList[edge.target] = [] + } + reverseAdjList[edge.target].push(edge.source) + } + + // BFS to find all ancestors and their shortest distance from target + while (queue.length > 0) { + const [currentNodeId, distance] = queue.shift()! + + if (visited.has(currentNodeId)) { + // If we've seen this node before, update its distance if this path is shorter + const currentDistance = nodeDistances.get(currentNodeId) || Infinity + if (distance < currentDistance) { + nodeDistances.set(currentNodeId, distance) + } + continue + } + + visited.add(currentNodeId) + nodeDistances.set(currentNodeId, distance) + + // Don't add the target node itself to the results + if (currentNodeId !== targetNodeId) { + pathNodes.add(currentNodeId) + } + + // Get all incoming edges from the reverse adjacency list + const incomingNodeIds = reverseAdjList[currentNodeId] || [] + + // Add all source nodes to the queue with incremented distance + for (const sourceId of incomingNodeIds) { + queue.push([sourceId, distance + 1]) + } + } + + return Array.from(pathNodes) +} + export function useBlockConnections(blockId: string) { const { edges, blocks } = useWorkflowStore( (state) => ({ @@ -62,7 +119,51 @@ export function useBlockConnections(blockId: string) { shallow ) - const incomingConnections = edges + // Find all blocks along paths leading to this block + const allPathNodeIds = findAllPathNodes(edges, blockId) + + // Map each path node to a ConnectedBlock structure + const allPathConnections = allPathNodeIds.map(sourceId => { + const sourceBlock = blocks[sourceId] + if (!sourceBlock) return null + + // Get the response format from the subblock store + const responseFormatValue = useSubBlockStore + .getState() + .getValue(sourceId, 'responseFormat') + + let responseFormat + + try { + responseFormat = + typeof responseFormatValue === 'string' && responseFormatValue + ? JSON.parse(responseFormatValue) + : responseFormatValue // Handle case where it's already an object + } catch (e) { + logger.error('Failed to parse response format:', { e }) + responseFormat = undefined + } + + // Get the default output type from the block's outputs + const defaultOutputs: Field[] = Object.entries(sourceBlock.outputs || {}).map(([key]) => ({ + name: key, + type: 'string', + })) + + // Extract fields from the response format using our helper function + const outputFields = responseFormat ? extractFieldsFromSchema(responseFormat) : defaultOutputs + + return { + id: sourceBlock.id, + type: sourceBlock.type, + outputType: outputFields.map((field: Field) => field.name), + name: sourceBlock.name, + responseFormat, + } + }).filter(Boolean) as ConnectedBlock[] + + // Keep the original incoming connections for compatibility + const directIncomingConnections = edges .filter((edge) => edge.target === blockId) .map((edge) => { const sourceBlock = blocks[edge.source] @@ -103,7 +204,8 @@ export function useBlockConnections(blockId: string) { }) return { - incomingConnections, - hasIncomingConnections: incomingConnections.length > 0, + incomingConnections: allPathConnections, + directIncomingConnections, + hasIncomingConnections: allPathConnections.length > 0, } } diff --git a/sim/app/w/[id]/layout.tsx b/sim/app/w/[id]/layout.tsx index d5aa91522f..4c40bff5d6 100644 --- a/sim/app/w/[id]/layout.tsx +++ b/sim/app/w/[id]/layout.tsx @@ -9,7 +9,7 @@ export default function WorkflowLayout({ children }: { children: React.ReactNode <> - + {/* */}
{children} diff --git a/sim/app/w/logs/components/filters/components/level.tsx b/sim/app/w/logs/components/filters/components/level.tsx index b8f92c33cb..54abcb9027 100644 --- a/sim/app/w/logs/components/filters/components/level.tsx +++ b/sim/app/w/logs/components/filters/components/level.tsx @@ -34,7 +34,10 @@ export default function Level() { {levels.map((levelItem) => ( setLevel(levelItem.value)} + onSelect={(e) => { + e.preventDefault() + setLevel(levelItem.value) + }} className="flex items-center justify-between p-2 cursor-pointer text-sm" >
diff --git a/sim/app/w/logs/components/filters/components/timeline.tsx b/sim/app/w/logs/components/filters/components/timeline.tsx index c037a11234..ed5a52e9ae 100644 --- a/sim/app/w/logs/components/filters/components/timeline.tsx +++ b/sim/app/w/logs/components/filters/components/timeline.tsx @@ -25,7 +25,10 @@ export default function Timeline() { {timeRanges.map((range) => ( setTimeRange(range)} + onSelect={(e) => { + e.preventDefault() + setTimeRange(range) + }} className="flex items-center justify-between p-2 cursor-pointer text-sm" > {range} diff --git a/sim/app/w/logs/components/filters/components/workflow.tsx b/sim/app/w/logs/components/filters/components/workflow.tsx index 55a63561d3..dcf3343db4 100644 --- a/sim/app/w/logs/components/filters/components/workflow.tsx +++ b/sim/app/w/logs/components/filters/components/workflow.tsx @@ -59,10 +59,13 @@ export default function Workflow() { - + { + e.preventDefault() + clearSelections() + }} className="flex items-center justify-between p-2 cursor-pointer text-sm" > All workflows @@ -74,7 +77,10 @@ export default function Workflow() { {workflows.map((workflow) => ( toggleWorkflowId(workflow.id)} + onSelect={(e) => { + e.preventDefault() + toggleWorkflowId(workflow.id) + }} className="flex items-center justify-between p-2 cursor-pointer text-sm" >
diff --git a/sim/app/w/logs/components/filters/filters.tsx b/sim/app/w/logs/components/filters/filters.tsx index cb6ed40e47..58f88f9511 100644 --- a/sim/app/w/logs/components/filters/filters.tsx +++ b/sim/app/w/logs/components/filters/filters.tsx @@ -10,7 +10,7 @@ import Workflow from './components/workflow' */ export function Filters() { return ( -
+

Filters

{/* Timeline Filter */} diff --git a/sim/app/w/marketplace/components/toolbar/toolbar.tsx b/sim/app/w/marketplace/components/toolbar/toolbar.tsx index 940171fd3b..0ba7a81108 100644 --- a/sim/app/w/marketplace/components/toolbar/toolbar.tsx +++ b/sim/app/w/marketplace/components/toolbar/toolbar.tsx @@ -37,19 +37,19 @@ export function Toolbar({ scrollToSection, activeSection }: ToolbarProps) { // Set categories including special sections useEffect(() => { // Start with special sections like 'popular' and 'recent' - const specialSections = ['popular'] + const specialSections = ['popular', 'recent'] // Add categories from centralized definitions const categoryValues = CATEGORIES.map((cat) => cat.value) - // Add 'recent' as the last item - const allCategories = [...specialSections, ...categoryValues, 'recent'] + // Put special sections first, then regular categories + const allCategories = [...specialSections, ...categoryValues] setCategories(allCategories) }, []) return ( -
+

Categories