improvement(ui/ux): connections and styling

This commit is contained in:
Emir Karabeg
2025-03-24 17:22:20 -07:00
parent 91327af5ba
commit 16458c7e5d
14 changed files with 168 additions and 49 deletions
@@ -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 = () => (
<div className="flex items-center">
{showRunProgress && (
<div className="mr-3 w-28">
<Progress value={(completedRuns / runCount) * 100} className="h-2 bg-muted" />
<p className="text-xs text-muted-foreground mt-1 text-center">
{completedRuns}/{runCount} runs
</p>
</div>
)}
<div className="flex ml-1">
{/* Main Run Button */}
<Button
@@ -603,12 +588,12 @@ export function ControlBar() {
(isExecuting || isMultiRunning) &&
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
'rounded-r-none border-r border-r-[#6420cc] py-2 px-4 h-10'
'rounded-r-none border-r border-r-[#6420cc] py-1.5 px-3 h-10 text-sm rounded-l-sm'
)}
onClick={handleMultipleRuns}
disabled={isExecuting || isMultiRunning}
>
<Play className={cn('h-3.5 w-3.5', 'fill-current stroke-current')} />
<Play className={cn('!h-3 !w-3', 'fill-current stroke-current')} />
{isMultiRunning
? `Running ${completedRuns}/${runCount}`
: isExecuting
@@ -623,26 +608,26 @@ export function ControlBar() {
<DropdownMenuTrigger asChild>
<Button
className={cn(
'px-2 font-medium',
'px-1.5 font-medium',
'bg-[#7F2FFF] hover:bg-[#7028E6]',
'shadow-[0_0_0_0_#7F2FFF] hover:shadow-[0_0_0_4px_rgba(127,47,255,0.15)]',
'text-white transition-all duration-200',
(isExecuting || isMultiRunning) &&
'relative after:absolute after:inset-0 after:animate-pulse after:bg-white/20',
'disabled:opacity-50 disabled:hover:bg-[#7F2FFF] disabled:hover:shadow-none',
'rounded-l-none h-10'
'rounded-l-none h-10 rounded-r-sm'
)}
disabled={isExecuting || isMultiRunning}
>
<ChevronDown className="h-4 w-4" />
<ChevronDown className="h-3.5 w-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-20">
<DropdownMenuContent align="end" className="min-w-20">
{RUN_COUNT_OPTIONS.map((count) => (
<DropdownMenuItem
key={count}
onClick={() => setRunCount(count)}
className={cn('justify-center', runCount === count && 'bg-muted')}
className={cn('justify-center cursor-pointer', runCount === count && 'bg-muted')}
>
{count}
</DropdownMenuItem>
@@ -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"
>
<div
className="relative flex h-10 w-10 shrink-0 items-center justify-center overflow-hidden rounded-lg"
@@ -46,14 +46,14 @@ export function Toolbar() {
}
return (
<div className="fixed left-14 top-16 z-10 h-[calc(100vh-4rem)] w-64 border-r bg-background sm:block">
<div className="fixed left-14 top-16 z-10 h-[calc(100vh-4rem)] w-60 border-r bg-background sm:block">
<div className="flex flex-col h-full">
<div className="px-4 pt-4 pb-1 sticky top-0 bg-background z-20">
<div className="relative">
<Search className="absolute left-3 top-[50%] h-4 w-4 -translate-y-[50%] text-muted-foreground" />
<Input
placeholder="Search..."
className="pl-9"
className="pl-9 rounded-md"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
autoComplete="off"
@@ -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<string, ConnectedBlock>
)
// 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 (
<div className="absolute -left-[180px] top-0 space-y-2 flex flex-col items-end w-[160px]">
{incomingConnections.map((connection) => (
<div className="absolute -left-[240px] top-0 space-y-2 flex flex-col items-end w-[220px] max-h-[400px] overflow-y-auto">
{sortedConnections.map((connection) => (
<div key={connection.id} className="space-y-2">
{Array.isArray(connection.outputType) ? (
// Handle array of field names
@@ -44,7 +44,7 @@ export function Dropdown({ options, defaultValue, blockId, subBlockId }: Dropdow
<SelectTrigger className="text-left">
<SelectValue placeholder="Select an option" />
</SelectTrigger>
<SelectContent>
<SelectContent className="max-h-48">
{options.map((option) => (
<SelectItem key={getOptionValue(option)} value={getOptionValue(option)}>
{getOptionLabel(option)}
+105 -3
View File
@@ -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<string, number>()
const visited = new Set<string>()
const queue: [string, number][] = [[targetNodeId, 0]] // [nodeId, distance]
const pathNodes = new Set<string>()
// Build a reverse adjacency list for faster traversal
const reverseAdjList: Record<string, string[]> = {}
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,
}
}
+1 -1
View File
@@ -9,7 +9,7 @@ export default function WorkflowLayout({ children }: { children: React.ReactNode
<>
<ControlBar />
<Toolbar />
<Chat />
{/* <Chat /> */}
<Console />
<main className="grid items-start gap-2 bg-muted/40 h-[calc(100vh-4rem)]">
<ErrorBoundary>{children}</ErrorBoundary>
@@ -34,7 +34,10 @@ export default function Level() {
{levels.map((levelItem) => (
<DropdownMenuItem
key={levelItem.value}
onClick={() => setLevel(levelItem.value)}
onSelect={(e) => {
e.preventDefault()
setLevel(levelItem.value)
}}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<div className="flex items-center">
@@ -25,7 +25,10 @@ export default function Timeline() {
{timeRanges.map((range) => (
<DropdownMenuItem
key={range}
onClick={() => setTimeRange(range)}
onSelect={(e) => {
e.preventDefault()
setTimeRange(range)
}}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<span>{range}</span>
@@ -59,10 +59,13 @@ export default function Workflow() {
<ChevronDown className="h-4 w-4 ml-2 text-muted-foreground" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="start" className="w-[220px] max-h-[300px] overflow-y-auto">
<DropdownMenuContent align="start" className="w-[180px] max-h-[300px] overflow-y-auto">
<DropdownMenuItem
key="all"
onClick={clearSelections}
onSelect={(e) => {
e.preventDefault()
clearSelections()
}}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<span>All workflows</span>
@@ -74,7 +77,10 @@ export default function Workflow() {
{workflows.map((workflow) => (
<DropdownMenuItem
key={workflow.id}
onClick={() => toggleWorkflowId(workflow.id)}
onSelect={(e) => {
e.preventDefault()
toggleWorkflowId(workflow.id)
}}
className="flex items-center justify-between p-2 cursor-pointer text-sm"
>
<div className="flex items-center">
@@ -10,7 +10,7 @@ import Workflow from './components/workflow'
*/
export function Filters() {
return (
<div className="p-4 w-64 border-r h-full overflow-auto">
<div className="p-4 w-60 border-r h-full overflow-auto">
<h2 className="text-sm font-medium mb-4 pl-2">Filters</h2>
{/* Timeline Filter */}
@@ -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 (
<div className="p-4 w-64 border-r h-full overflow-auto">
<div className="p-4 w-60 border-r h-full overflow-auto">
<h2 className="text-sm font-medium mb-4 pl-2">Categories</h2>
<nav className="space-y-1">
{categories.map((category) => (
+1 -1
View File
@@ -16,7 +16,7 @@ type GitHubResponse =
export const GitHubBlock: BlockConfig<GitHubResponse> = {
type: 'github',
name: 'GitHub',
description: 'Interact with GitHub repositories and PRs',
description: 'Interact with GitHub',
longDescription:
'Access GitHub repositories, pull requests, and comments through the GitHub API. Automate code reviews, PR management, and repository interactions within your workflow.',
category: 'tools',
+12 -6
View File
@@ -25,7 +25,7 @@ const SelectTrigger = React.forwardRef<
>
{children}
<SelectPrimitive.Icon asChild>
<ChevronDown className="h-4 w-4 opacity-50" />
<ChevronDown className="h-4 w-4 opacity-50 transition-transform duration-200 ease-in-out" />
</SelectPrimitive.Icon>
</SelectPrimitive.Trigger>
))
@@ -37,10 +37,13 @@ const SelectScrollUpButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollUpButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
className={cn(
'flex cursor-default items-center justify-center py-1 bg-popover sticky top-0 z-10',
className
)}
{...props}
>
<ChevronUp className="h-4 w-4" />
<ChevronUp className="h-4 w-4 opacity-70 hover:opacity-100 transition-opacity" />
</SelectPrimitive.ScrollUpButton>
))
SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
@@ -51,10 +54,13 @@ const SelectScrollDownButton = React.forwardRef<
>(({ className, ...props }, ref) => (
<SelectPrimitive.ScrollDownButton
ref={ref}
className={cn('flex cursor-default items-center justify-center py-1', className)}
className={cn(
'flex cursor-default items-center justify-center py-1 bg-popover sticky bottom-0 z-10',
className
)}
{...props}
>
<ChevronDown className="h-4 w-4" />
<ChevronDown className="h-4 w-4 opacity-70 hover:opacity-100 transition-opacity" />
</SelectPrimitive.ScrollDownButton>
))
SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
@@ -78,7 +84,7 @@ const SelectContent = React.forwardRef<
<SelectScrollUpButton />
<SelectPrimitive.Viewport
className={cn(
'p-1',
'p-1 scrollbar-thin scrollbar-thumb-slate-200 scrollbar-track-transparent',
position === 'popper' &&
'h-[var(--radix-select-trigger-height)] w-full min-w-[var(--radix-select-trigger-width)]'
)}