mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
improvement: runpath edges, blocks, active (#2008)
This commit is contained in:
@@ -74,30 +74,6 @@
|
||||
animation: dash-animation 1.5s linear infinite !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Active block ring animation - cycles through gray tones using box-shadow
|
||||
*/
|
||||
@keyframes ring-pulse-colors {
|
||||
0%,
|
||||
100% {
|
||||
box-shadow: 0 0 0 4px var(--surface-14);
|
||||
}
|
||||
33% {
|
||||
box-shadow: 0 0 0 4px var(--surface-12);
|
||||
}
|
||||
66% {
|
||||
box-shadow: 0 0 0 4px var(--surface-15);
|
||||
}
|
||||
}
|
||||
|
||||
.dark .animate-ring-pulse {
|
||||
animation: ring-pulse-colors 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
.light .animate-ring-pulse {
|
||||
animation: ring-pulse-colors 2s ease-in-out infinite !important;
|
||||
}
|
||||
|
||||
/**
|
||||
* Dark color tokens - single source of truth for all colors (dark-only)
|
||||
*/
|
||||
@@ -135,6 +111,7 @@
|
||||
--border-strong: #d1d1d1;
|
||||
--divider: #e5e5e5;
|
||||
--border-muted: #eeeeee;
|
||||
--border-success: #d5d5d5;
|
||||
|
||||
/* Brand & state */
|
||||
--brand-400: #8e4cfb;
|
||||
@@ -250,6 +227,7 @@
|
||||
--border-strong: #303030;
|
||||
--divider: #393939;
|
||||
--border-muted: #424242;
|
||||
--border-success: #575757;
|
||||
|
||||
/* Brand & state */
|
||||
--brand-400: #8e4cfb;
|
||||
|
||||
+18
-4
@@ -343,6 +343,7 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
handleClick,
|
||||
hasRing,
|
||||
ringStyles,
|
||||
runPathStatus,
|
||||
} = useBlockCore({ blockId: id, data, isPending })
|
||||
|
||||
const currentBlock = currentWorkflow.getBlockById(id)
|
||||
@@ -750,21 +751,26 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
e.stopPropagation()
|
||||
}}
|
||||
>
|
||||
<div className='flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||
<div className='relative z-10 flex min-w-0 flex-1 items-center gap-[10px]'>
|
||||
<div
|
||||
className='flex h-[24px] w-[24px] flex-shrink-0 items-center justify-center rounded-[6px]'
|
||||
style={{ backgroundColor: isEnabled ? config.bgColor : 'gray' }}
|
||||
style={{
|
||||
backgroundColor: isEnabled ? config.bgColor : 'gray',
|
||||
}}
|
||||
>
|
||||
<config.icon className='h-[16px] w-[16px] text-white' />
|
||||
</div>
|
||||
<span
|
||||
className={cn('truncate font-medium text-[16px]', !isEnabled && 'text-[#808080]')}
|
||||
className={cn(
|
||||
'truncate font-medium text-[16px]',
|
||||
!isEnabled && runPathStatus !== 'success' && 'text-[#808080]'
|
||||
)}
|
||||
title={name}
|
||||
>
|
||||
{name}
|
||||
</span>
|
||||
</div>
|
||||
<div className='flex flex-shrink-0 items-center gap-2'>
|
||||
<div className='relative z-10 flex flex-shrink-0 items-center gap-2'>
|
||||
{isWorkflowSelector && childWorkflowId && (
|
||||
<>
|
||||
{typeof childIsDeployed === 'boolean' ? (
|
||||
@@ -890,6 +896,14 @@ export const WorkflowBlock = memo(function WorkflowBlock({
|
||||
</Tooltip.Content>
|
||||
</Tooltip.Root>
|
||||
)}
|
||||
{/* {isActive && (
|
||||
<div className='mr-[2px] ml-2 flex h-[16px] w-[16px] items-center justify-center'>
|
||||
<div
|
||||
className='h-full w-full animate-spin-slow rounded-full border-[2.5px] border-[rgba(255,102,0,0.25)] border-t-[var(--warning)]'
|
||||
aria-hidden='true'
|
||||
/>
|
||||
</div>
|
||||
)} */}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
+8
@@ -1,6 +1,7 @@
|
||||
import { X } from 'lucide-react'
|
||||
import { BaseEdge, EdgeLabelRenderer, type EdgeProps, getSmoothStepPath } from 'reactflow'
|
||||
import type { EdgeDiffStatus } from '@/lib/workflows/diff/types'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { useWorkflowDiffStore } from '@/stores/workflow-diff'
|
||||
|
||||
interface WorkflowEdgeProps extends EdgeProps {
|
||||
@@ -43,6 +44,7 @@ export const WorkflowEdge = ({
|
||||
const diffAnalysis = useWorkflowDiffStore((state) => state.diffAnalysis)
|
||||
const isShowingDiff = useWorkflowDiffStore((state) => state.isShowingDiff)
|
||||
const isDiffReady = useWorkflowDiffStore((state) => state.isDiffReady)
|
||||
const lastRunEdges = useExecutionStore((state) => state.lastRunEdges)
|
||||
|
||||
const generateEdgeIdentity = (
|
||||
sourceId: string,
|
||||
@@ -78,10 +80,16 @@ export const WorkflowEdge = ({
|
||||
const dataSourceHandle = (data as { sourceHandle?: string } | undefined)?.sourceHandle
|
||||
const isErrorEdge = (sourceHandle ?? dataSourceHandle) === 'error'
|
||||
|
||||
// Check if this edge was traversed during last execution
|
||||
const edgeRunStatus = lastRunEdges.get(id)
|
||||
|
||||
const getEdgeColor = () => {
|
||||
if (edgeDiffStatus === 'deleted') return 'var(--text-error)'
|
||||
if (isErrorEdge) return 'var(--text-error)'
|
||||
if (edgeDiffStatus === 'new') return 'var(--brand-tertiary)'
|
||||
// Show run path status if edge was traversed
|
||||
if (edgeRunStatus === 'success') return 'var(--border-success)'
|
||||
if (edgeRunStatus === 'error') return 'var(--text-error)'
|
||||
return 'var(--surface-12)'
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { cn } from '@/lib/utils'
|
||||
import { useExecutionStore } from '@/stores/execution/store'
|
||||
import { usePanelEditorStore } from '@/stores/panel-new/editor/store'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useBlockState } from '../components/workflow-block/hooks'
|
||||
import type { WorkflowBlockProps } from '../components/workflow-block/types'
|
||||
import { getBlockRingStyles } from '../utils/block-ring-utils'
|
||||
import { useCurrentWorkflow } from './use-current-workflow'
|
||||
|
||||
interface UseBlockCoreOptions {
|
||||
@@ -43,60 +43,19 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
|
||||
}, [blockId, setCurrentBlockId])
|
||||
|
||||
// Ring styling based on all states
|
||||
// Priority: active (animated) > pending > focused > deleted > diff > run path
|
||||
const { hasRing, ringStyles } = useMemo(() => {
|
||||
const hasRing =
|
||||
isActive ||
|
||||
isPending ||
|
||||
isFocused ||
|
||||
diffStatus === 'new' ||
|
||||
diffStatus === 'edited' ||
|
||||
isDeletedBlock ||
|
||||
!!runPathStatus
|
||||
|
||||
const ringStyles = cn(
|
||||
// Executing block: animated ring cycling through gray tones (animation handles all styling)
|
||||
isActive && 'animate-ring-pulse',
|
||||
// Non-active states use standard ring utilities
|
||||
!isActive && hasRing && 'ring-[1.75px]',
|
||||
// Pending state: warning ring
|
||||
!isActive && isPending && 'ring-[var(--warning)]',
|
||||
// Focused (selected) state: brand ring
|
||||
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
|
||||
// Deleted state (highest priority after active/pending/focused)
|
||||
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
|
||||
// Diff states
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'new' &&
|
||||
'ring-[#22C55E]',
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'edited' &&
|
||||
'ring-[var(--warning)]',
|
||||
// Run path states (lowest priority - only show if no other states active)
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
runPathStatus === 'success' &&
|
||||
'ring-[var(--surface-14)]',
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
runPathStatus === 'error' &&
|
||||
'ring-[var(--text-error)]'
|
||||
)
|
||||
|
||||
return { hasRing, ringStyles }
|
||||
}, [isActive, isPending, isFocused, diffStatus, isDeletedBlock, runPathStatus])
|
||||
// Priority: active (executing) > pending > focused > deleted > diff > run path
|
||||
const { hasRing, ringClassName: ringStyles } = useMemo(
|
||||
() =>
|
||||
getBlockRingStyles({
|
||||
isActive,
|
||||
isPending,
|
||||
isFocused,
|
||||
isDeletedBlock,
|
||||
diffStatus,
|
||||
runPathStatus,
|
||||
}),
|
||||
[isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus]
|
||||
)
|
||||
|
||||
return {
|
||||
// Workflow context
|
||||
@@ -116,5 +75,6 @@ export function useBlockCore({ blockId, data, isPending = false }: UseBlockCoreO
|
||||
// Ring styling
|
||||
hasRing,
|
||||
ringStyles,
|
||||
runPathStatus,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,6 +100,7 @@ export function useWorkflowExecution() {
|
||||
setDebugContext,
|
||||
setActiveBlocks,
|
||||
setBlockRunStatus,
|
||||
setEdgeRunStatus,
|
||||
} = useExecutionStore()
|
||||
const [executionResult, setExecutionResult] = useState<ExecutionResult | null>(null)
|
||||
const executionStream = useExecutionStream()
|
||||
@@ -892,6 +893,12 @@ export function useWorkflowExecution() {
|
||||
activeBlocksSet.add(data.blockId)
|
||||
// Create a new Set to trigger React re-render
|
||||
setActiveBlocks(new Set(activeBlocksSet))
|
||||
|
||||
// Track edges that led to this block as soon as execution starts
|
||||
const incomingEdges = workflowEdges.filter((edge) => edge.target === data.blockId)
|
||||
incomingEdges.forEach((edge) => {
|
||||
setEdgeRunStatus(edge.id, 'success')
|
||||
})
|
||||
},
|
||||
|
||||
onBlockCompleted: (data) => {
|
||||
@@ -904,6 +911,8 @@ export function useWorkflowExecution() {
|
||||
// Track successful block execution in run path
|
||||
setBlockRunStatus(data.blockId, 'success')
|
||||
|
||||
// Edges already tracked in onBlockStarted, no need to track again
|
||||
|
||||
// Add to console
|
||||
addConsole({
|
||||
input: data.input || {},
|
||||
@@ -938,7 +947,6 @@ export function useWorkflowExecution() {
|
||||
|
||||
// Track failed block execution in run path
|
||||
setBlockRunStatus(data.blockId, 'error')
|
||||
|
||||
// Add error to console
|
||||
addConsole({
|
||||
input: data.input || {},
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
import { cn } from '@/lib/utils'
|
||||
|
||||
export type BlockDiffStatus = 'new' | 'edited' | null | undefined
|
||||
|
||||
export type BlockRunPathStatus = 'success' | 'error' | undefined
|
||||
|
||||
export interface BlockRingOptions {
|
||||
isActive: boolean
|
||||
isPending: boolean
|
||||
isFocused: boolean
|
||||
isDeletedBlock: boolean
|
||||
diffStatus: BlockDiffStatus
|
||||
runPathStatus: BlockRunPathStatus
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives visual ring visibility and class names for workflow blocks
|
||||
* based on execution, focus, diff, deletion, and run-path states.
|
||||
*/
|
||||
export function getBlockRingStyles(options: BlockRingOptions): {
|
||||
hasRing: boolean
|
||||
ringClassName: string
|
||||
} {
|
||||
const { isActive, isPending, isFocused, isDeletedBlock, diffStatus, runPathStatus } = options
|
||||
|
||||
const hasRing =
|
||||
isActive ||
|
||||
isPending ||
|
||||
isFocused ||
|
||||
diffStatus === 'new' ||
|
||||
diffStatus === 'edited' ||
|
||||
isDeletedBlock ||
|
||||
!!runPathStatus
|
||||
|
||||
const ringClassName = cn(
|
||||
// Executing block: pulsing success ring with prominent thickness
|
||||
isActive && 'ring-[3.5px] ring-[var(--border-success)] animate-ring-pulse',
|
||||
// Non-active states use standard ring utilities
|
||||
!isActive && hasRing && 'ring-[1.75px]',
|
||||
// Pending state: warning ring
|
||||
!isActive && isPending && 'ring-[var(--warning)]',
|
||||
// Focused (selected) state: brand ring
|
||||
!isActive && !isPending && isFocused && 'ring-[var(--brand-secondary)]',
|
||||
// Deleted state (highest priority after active/pending/focused)
|
||||
!isActive && !isPending && !isFocused && isDeletedBlock && 'ring-[var(--text-error)]',
|
||||
// Diff states
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'new' &&
|
||||
'ring-[#22C55E]',
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
diffStatus === 'edited' &&
|
||||
'ring-[var(--warning)]',
|
||||
// Run path states (lowest priority - only show if no other states active)
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
runPathStatus === 'success' &&
|
||||
'ring-[var(--border-success)]',
|
||||
!isActive &&
|
||||
!isPending &&
|
||||
!isFocused &&
|
||||
!isDeletedBlock &&
|
||||
!diffStatus &&
|
||||
runPathStatus === 'error' &&
|
||||
'ring-[var(--text-error)]'
|
||||
)
|
||||
|
||||
return { hasRing, ringClassName }
|
||||
}
|
||||
@@ -1,2 +1,3 @@
|
||||
export * from './auto-layout-utils'
|
||||
export * from './block-ring-utils'
|
||||
export * from './workflow-execution-utils'
|
||||
|
||||
@@ -56,7 +56,7 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
|
||||
if (isExecuting) {
|
||||
set({ autoPanDisabled: false })
|
||||
// Clear run path when starting a new execution
|
||||
set({ lastRunPath: new Map() })
|
||||
set({ lastRunPath: new Map(), lastRunEdges: new Map() })
|
||||
}
|
||||
},
|
||||
setIsDebugging: (isDebugging) => set({ isDebugging }),
|
||||
@@ -69,6 +69,12 @@ export const useExecutionStore = create<ExecutionState & ExecutionActions>()((se
|
||||
newRunPath.set(blockId, status)
|
||||
set({ lastRunPath: newRunPath })
|
||||
},
|
||||
clearRunPath: () => set({ lastRunPath: new Map() }),
|
||||
setEdgeRunStatus: (edgeId, status) => {
|
||||
const { lastRunEdges } = get()
|
||||
const newRunEdges = new Map(lastRunEdges)
|
||||
newRunEdges.set(edgeId, status)
|
||||
set({ lastRunEdges: newRunEdges })
|
||||
},
|
||||
clearRunPath: () => set({ lastRunPath: new Map(), lastRunEdges: new Map() }),
|
||||
reset: () => set(initialState),
|
||||
}))
|
||||
|
||||
@@ -6,6 +6,11 @@ import type { ExecutionContext } from '@/executor/types'
|
||||
*/
|
||||
export type BlockRunStatus = 'success' | 'error'
|
||||
|
||||
/**
|
||||
* Represents the execution result of an edge in the last run
|
||||
*/
|
||||
export type EdgeRunStatus = 'success' | 'error'
|
||||
|
||||
export interface ExecutionState {
|
||||
activeBlockIds: Set<string>
|
||||
isExecuting: boolean
|
||||
@@ -16,9 +21,14 @@ export interface ExecutionState {
|
||||
autoPanDisabled: boolean
|
||||
/**
|
||||
* Tracks blocks from the last execution run and their success/error status.
|
||||
* Cleared when a new run starts. Used to show run path indicators (green/red rings).
|
||||
* Cleared when a new run starts. Used to show run path indicators (rings on blocks).
|
||||
*/
|
||||
lastRunPath: Map<string, BlockRunStatus>
|
||||
/**
|
||||
* Tracks edges from the last execution run and their success/error status.
|
||||
* Cleared when a new run starts. Used to show run path indicators on edges.
|
||||
*/
|
||||
lastRunEdges: Map<string, EdgeRunStatus>
|
||||
}
|
||||
|
||||
export interface ExecutionActions {
|
||||
@@ -30,6 +40,7 @@ export interface ExecutionActions {
|
||||
setDebugContext: (context: ExecutionContext | null) => void
|
||||
setAutoPanDisabled: (disabled: boolean) => void
|
||||
setBlockRunStatus: (blockId: string, status: BlockRunStatus) => void
|
||||
setEdgeRunStatus: (edgeId: string, status: EdgeRunStatus) => void
|
||||
clearRunPath: () => void
|
||||
reset: () => void
|
||||
}
|
||||
@@ -43,6 +54,7 @@ export const initialState: ExecutionState = {
|
||||
debugContext: null,
|
||||
autoPanDisabled: false,
|
||||
lastRunPath: new Map(),
|
||||
lastRunEdges: new Map(),
|
||||
}
|
||||
|
||||
// Types for panning functionality
|
||||
|
||||
@@ -144,10 +144,10 @@ export default {
|
||||
},
|
||||
'ring-pulse': {
|
||||
'0%, 100%': {
|
||||
opacity: '1',
|
||||
'box-shadow': '0 0 0 1.5px var(--border-success)',
|
||||
},
|
||||
'50%': {
|
||||
opacity: '0.6',
|
||||
'box-shadow': '0 0 0 4px var(--border-success)',
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user