feat: error connections
This commit is contained in:
@@ -274,6 +274,7 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
const pathUpdateEndTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
const pathUpdateEndTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||||
|
|
||||||
const [connectionPathPausedNodeIds, setConnectionPathPausedNodeIds] = React.useState<string[]>([])
|
const [connectionPathPausedNodeIds, setConnectionPathPausedNodeIds] = React.useState<string[]>([])
|
||||||
|
const [connectionPathErrorNodeIds, setConnectionPathErrorNodeIds] = React.useState<string[]>([])
|
||||||
const connectionPathPausedNodeIdsRef = useRef<string[]>([])
|
const connectionPathPausedNodeIdsRef = useRef<string[]>([])
|
||||||
connectionPathPausedNodeIdsRef.current = connectionPathPausedNodeIds
|
connectionPathPausedNodeIdsRef.current = connectionPathPausedNodeIds
|
||||||
|
|
||||||
@@ -586,6 +587,14 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
setConnectionPathPausedNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
setConnectionPathPausedNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const addConnectionPathError = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathErrorNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
const removeConnectionPathError = useCallback((nodeId: string) => {
|
||||||
|
setConnectionPathErrorNodeIds((prev) => prev.filter((id) => id !== nodeId))
|
||||||
|
}, [])
|
||||||
|
|
||||||
const flowContextValue = useMemo(
|
const flowContextValue = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
nodes,
|
nodes,
|
||||||
@@ -609,6 +618,9 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
connectionPathPausedNodeIds,
|
connectionPathPausedNodeIds,
|
||||||
addConnectionPathPausedNode,
|
addConnectionPathPausedNode,
|
||||||
removeConnectionPathPausedNode,
|
removeConnectionPathPausedNode,
|
||||||
|
connectionPathErrorNodeIds,
|
||||||
|
addConnectionPathError,
|
||||||
|
removeConnectionPathError,
|
||||||
startConnectionPathUpdate,
|
startConnectionPathUpdate,
|
||||||
endConnectionPathUpdate,
|
endConnectionPathUpdate,
|
||||||
}),
|
}),
|
||||||
@@ -634,6 +646,9 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
|||||||
connectionPathPausedNodeIds,
|
connectionPathPausedNodeIds,
|
||||||
addConnectionPathPausedNode,
|
addConnectionPathPausedNode,
|
||||||
removeConnectionPathPausedNode,
|
removeConnectionPathPausedNode,
|
||||||
|
connectionPathErrorNodeIds,
|
||||||
|
addConnectionPathError,
|
||||||
|
removeConnectionPathError,
|
||||||
startConnectionPathUpdate,
|
startConnectionPathUpdate,
|
||||||
endConnectionPathUpdate,
|
endConnectionPathUpdate,
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
type EdgeProps,
|
type EdgeProps,
|
||||||
} from '@xyflow/react'
|
} from '@xyflow/react'
|
||||||
import FlowContext from '../../lib/flowContext'
|
import FlowContext from '../../lib/flowContext'
|
||||||
|
import { getConnectionStatus, CONNECTION_STATUS_CLASS } from '../../lib/connectionStatus'
|
||||||
import { getConnectionLabelForTarget } from '../../lib/nodeRegistry'
|
import { getConnectionLabelForTarget } from '../../lib/nodeRegistry'
|
||||||
|
|
||||||
const EDGE_STROKE_WIDTH = 2
|
const EDGE_STROKE_WIDTH = 2
|
||||||
@@ -30,6 +31,10 @@ export function AnimatedEdge({
|
|||||||
const pathNodeIds = ctx?.connectionPathNodeIds ?? EMPTY_PATH_NODE_IDS
|
const pathNodeIds = ctx?.connectionPathNodeIds ?? EMPTY_PATH_NODE_IDS
|
||||||
const pausedSegmentNodeIds = ctx?.connectionPathPausedSegmentNodeIds ?? EMPTY_PATH_NODE_IDS
|
const pausedSegmentNodeIds = ctx?.connectionPathPausedSegmentNodeIds ?? EMPTY_PATH_NODE_IDS
|
||||||
const activeSegmentNodeIds = ctx?.connectionPathActiveSegmentNodeIds ?? EMPTY_PATH_NODE_IDS
|
const activeSegmentNodeIds = ctx?.connectionPathActiveSegmentNodeIds ?? EMPTY_PATH_NODE_IDS
|
||||||
|
const errorTargetNodeIds = useMemo(
|
||||||
|
() => new Set(ctx?.connectionPathErrorNodeIds ?? []),
|
||||||
|
[ctx?.connectionPathErrorNodeIds]
|
||||||
|
)
|
||||||
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
const targetNode = useMemo(() => nodes.find((n: any) => n.id === target), [nodes, target])
|
||||||
const derivedLabel = useMemo(
|
const derivedLabel = useMemo(
|
||||||
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
||||||
@@ -37,10 +42,26 @@ export function AnimatedEdge({
|
|||||||
)
|
)
|
||||||
const label = labelProp ?? derivedLabel
|
const label = labelProp ?? derivedLabel
|
||||||
|
|
||||||
const isOnPausedSegment =
|
const connectionStatus = useMemo(
|
||||||
pathNodeIds.has(source) && pathNodeIds.has(target) && pausedSegmentNodeIds.has(source) && pausedSegmentNodeIds.has(target)
|
() =>
|
||||||
const isOnUpdatingPath =
|
getConnectionStatus({
|
||||||
pathNodeIds.has(source) && pathNodeIds.has(target) && activeSegmentNodeIds.has(source) && activeSegmentNodeIds.has(target)
|
source,
|
||||||
|
target,
|
||||||
|
pathNodeIds,
|
||||||
|
pausedSegmentNodeIds,
|
||||||
|
activeSegmentNodeIds,
|
||||||
|
errorTargetNodeIds,
|
||||||
|
}),
|
||||||
|
[
|
||||||
|
source,
|
||||||
|
target,
|
||||||
|
pathNodeIds,
|
||||||
|
pausedSegmentNodeIds,
|
||||||
|
activeSegmentNodeIds,
|
||||||
|
errorTargetNodeIds,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
const statusClass = CONNECTION_STATUS_CLASS[connectionStatus]
|
||||||
|
|
||||||
const [edgePath, edgeLabelX, edgeLabelY] = getBezierPath({
|
const [edgePath, edgeLabelX, edgeLabelY] = getBezierPath({
|
||||||
sourceX,
|
sourceX,
|
||||||
@@ -98,7 +119,7 @@ export function AnimatedEdge({
|
|||||||
strokeWidth: EDGE_STROKE_WIDTH,
|
strokeWidth: EDGE_STROKE_WIDTH,
|
||||||
...style,
|
...style,
|
||||||
}}
|
}}
|
||||||
className={`animated-edge-path${isOnPausedSegment ? ' animated-edge-path--paused' : isOnUpdatingPath ? ' animated-edge-path--updating' : ''}`}
|
className={`animated-edge-path${statusClass ? ` ${statusClass}` : ''}`}
|
||||||
interactionWidth={interactionWidth}
|
interactionWidth={interactionWidth}
|
||||||
/>
|
/>
|
||||||
{label != null && (
|
{label != null && (
|
||||||
|
|||||||
@@ -52,6 +52,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||||
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
||||||
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
||||||
|
const addConnectionPathError = flowContext?.addConnectionPathError
|
||||||
|
const removeConnectionPathError = flowContext?.removeConnectionPathError
|
||||||
const supportsFullscreen = getNodeType('render')?.supportsFullscreen
|
const supportsFullscreen = getNodeType('render')?.supportsFullscreen
|
||||||
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
const [renderedContent, setRenderedContent] = useState<string | null>(null)
|
||||||
const [resolvedContent, setResolvedContent] = useState<string | null>(null)
|
const [resolvedContent, setResolvedContent] = useState<string | null>(null)
|
||||||
@@ -78,6 +80,15 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
|||||||
const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : isAgentSource ? agentOutputMarkdown : ''
|
const sourceContent = srcNode?.type === 'config' ? getConfigContent((srcNode.data ?? undefined) as Record<string, unknown> | undefined) : isAgentSource ? agentOutputMarkdown : ''
|
||||||
const srcData = srcNode?.data ?? {}
|
const srcData = srcNode?.data ?? {}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!addConnectionPathError || !removeConnectionPathError) return
|
||||||
|
if (error != null) {
|
||||||
|
addConnectionPathError(id)
|
||||||
|
return () => removeConnectionPathError(id)
|
||||||
|
}
|
||||||
|
removeConnectionPathError(id)
|
||||||
|
}, [id, error, addConnectionPathError, removeConnectionPathError])
|
||||||
|
|
||||||
/** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */
|
/** Set of node IDs that can affect this render node (configs in the chain + variables/functions feeding them) */
|
||||||
const connectedNodeIds = useMemo(() => {
|
const connectedNodeIds = useMemo(() => {
|
||||||
const out = new Set<string>()
|
const out = new Set<string>()
|
||||||
|
|||||||
48
frontend/src/lib/connectionStatus.ts
Normal file
48
frontend/src/lib/connectionStatus.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
/**
|
||||||
|
* Connection status: visual state of an edge (color/class).
|
||||||
|
* Priority when multiple apply: error > paused > updating > default.
|
||||||
|
* Nodes report state via FlowContext (e.g. addConnectionPathError); edges derive status here.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export type ConnectionStatus = 'default' | 'updating' | 'paused' | 'error'
|
||||||
|
|
||||||
|
export type ConnectionStatusInputs = {
|
||||||
|
source: string
|
||||||
|
target: string
|
||||||
|
pathNodeIds: Set<string>
|
||||||
|
pausedSegmentNodeIds: Set<string>
|
||||||
|
activeSegmentNodeIds: Set<string>
|
||||||
|
errorTargetNodeIds: Set<string>
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Compute the single connection status for an edge (priority: error > paused > updating > default).
|
||||||
|
* Use in edge components; add new statuses by extending the type and adding a branch here.
|
||||||
|
*/
|
||||||
|
export function getConnectionStatus(inputs: ConnectionStatusInputs): ConnectionStatus {
|
||||||
|
const { target, pathNodeIds, pausedSegmentNodeIds, activeSegmentNodeIds, errorTargetNodeIds, source } = inputs
|
||||||
|
if (errorTargetNodeIds.has(target)) return 'error'
|
||||||
|
if (
|
||||||
|
pathNodeIds.has(source) &&
|
||||||
|
pathNodeIds.has(target) &&
|
||||||
|
pausedSegmentNodeIds.has(source) &&
|
||||||
|
pausedSegmentNodeIds.has(target)
|
||||||
|
)
|
||||||
|
return 'paused'
|
||||||
|
if (
|
||||||
|
pathNodeIds.has(source) &&
|
||||||
|
pathNodeIds.has(target) &&
|
||||||
|
activeSegmentNodeIds.has(source) &&
|
||||||
|
activeSegmentNodeIds.has(target)
|
||||||
|
)
|
||||||
|
return 'updating'
|
||||||
|
return 'default'
|
||||||
|
}
|
||||||
|
|
||||||
|
/** CSS class suffix for each status (animated-edge-path--{status}). */
|
||||||
|
export const CONNECTION_STATUS_CLASS: Record<ConnectionStatus, string> = {
|
||||||
|
default: '',
|
||||||
|
updating: 'animated-edge-path--updating',
|
||||||
|
paused: 'animated-edge-path--paused',
|
||||||
|
error: 'animated-edge-path--error',
|
||||||
|
}
|
||||||
@@ -46,6 +46,12 @@ export type FlowContextValue = {
|
|||||||
addConnectionPathPausedNode: (nodeId: string) => void
|
addConnectionPathPausedNode: (nodeId: string) => void
|
||||||
/** Remove this node from paused. */
|
/** Remove this node from paused. */
|
||||||
removeConnectionPathPausedNode: (nodeId: string) => void
|
removeConnectionPathPausedNode: (nodeId: string) => void
|
||||||
|
/** Node ids that have an error (e.g. Render node). Incoming edges show error status (red). */
|
||||||
|
connectionPathErrorNodeIds: string[]
|
||||||
|
/** Call when this node has an error; remove when error is cleared. */
|
||||||
|
addConnectionPathError: (nodeId: string) => void
|
||||||
|
/** Call when this node's error is cleared. */
|
||||||
|
removeConnectionPathError: (nodeId: string) => void
|
||||||
/** Call when a path update starts for this node. Animation runs at least CONNECTION_PATH_UPDATE_MIN_MS. */
|
/** Call when a path update starts for this node. Animation runs at least CONNECTION_PATH_UPDATE_MIN_MS. */
|
||||||
startConnectionPathUpdate: (nodeId: string) => void
|
startConnectionPathUpdate: (nodeId: string) => void
|
||||||
/** Call when a path update ends for this node. If min duration not reached, animation continues until then. */
|
/** Call when a path update ends for this node. If min duration not reached, animation continues until then. */
|
||||||
|
|||||||
@@ -162,6 +162,12 @@ body {
|
|||||||
transition: none;
|
transition: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__edge path.animated-edge-path.animated-edge-path--error,
|
||||||
|
.animated-edge-path.animated-edge-path--error {
|
||||||
|
stroke: hsl(0 70% 50%);
|
||||||
|
transition: none;
|
||||||
|
}
|
||||||
|
|
||||||
@keyframes edge-flow {
|
@keyframes edge-flow {
|
||||||
0% {
|
0% {
|
||||||
stroke-dashoffset: 14;
|
stroke-dashoffset: 14;
|
||||||
|
|||||||
Reference in New Issue
Block a user