Compare commits
3 Commits
3ef30bd58c
...
302107e710
| Author | SHA1 | Date | |
|---|---|---|---|
| 302107e710 | |||
| 48dcf38e01 | |||
| e79821bd81 |
@@ -66,7 +66,7 @@ import {
|
||||
getNodeType,
|
||||
isConnectionAllowed,
|
||||
} from '@/lib/nodeRegistry'
|
||||
import { getPathNodeIds } from '@/lib/graphPath'
|
||||
import { getPathNodeIds, getPausedSegmentNodeIds } from '@/lib/graphPath'
|
||||
import type { AppNode, AppEdge } from '@/lib/nodeTypes'
|
||||
import { toast } from 'sonner'
|
||||
import {
|
||||
@@ -273,9 +273,17 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
const pathUpdateStartTimeRef = useRef<number | null>(null)
|
||||
const pathUpdateEndTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
const [connectionPathPausedNodeIds, setConnectionPathPausedNodeIds] = React.useState<string[]>([])
|
||||
const [connectionPathErrorNodeIds, setConnectionPathErrorNodeIds] = React.useState<string[]>([])
|
||||
const connectionPathPausedNodeIdsRef = useRef<string[]>([])
|
||||
connectionPathPausedNodeIdsRef.current = connectionPathPausedNodeIds
|
||||
|
||||
const clearPathUpdateSession = useCallback(() => {
|
||||
setConnectionPathUpdatingNodeIds([])
|
||||
if (connectionPathPausedNodeIdsRef.current.length === 0) {
|
||||
setConnectionPathTriggerNodeIds([])
|
||||
setConnectionPathPausedNodeIds([])
|
||||
}
|
||||
}, [])
|
||||
|
||||
const startConnectionPathUpdate = useCallback((nodeId: string) => {
|
||||
@@ -544,10 +552,49 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
}, [])
|
||||
|
||||
const connectionPathNodeIds = useMemo(
|
||||
() => getPathNodeIds(edges, connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds),
|
||||
[edges, connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds]
|
||||
() =>
|
||||
getPathNodeIds(
|
||||
edges,
|
||||
connectionPathUpdatingNodeIds,
|
||||
connectionPathTriggerNodeIds,
|
||||
connectionPathPausedNodeIds
|
||||
),
|
||||
[edges, connectionPathUpdatingNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
||||
)
|
||||
|
||||
const connectionPathPausedSegmentNodeIds = useMemo(
|
||||
() =>
|
||||
getPausedSegmentNodeIds(
|
||||
edges,
|
||||
connectionPathNodeIds,
|
||||
connectionPathTriggerNodeIds,
|
||||
connectionPathPausedNodeIds
|
||||
),
|
||||
[edges, connectionPathNodeIds, connectionPathTriggerNodeIds, connectionPathPausedNodeIds]
|
||||
)
|
||||
|
||||
const connectionPathActiveSegmentNodeIds = useMemo(() => {
|
||||
const active = new Set(connectionPathNodeIds)
|
||||
connectionPathPausedSegmentNodeIds.forEach((id) => active.delete(id))
|
||||
return active
|
||||
}, [connectionPathNodeIds, connectionPathPausedSegmentNodeIds])
|
||||
|
||||
const addConnectionPathPausedNode = useCallback((nodeId: string) => {
|
||||
setConnectionPathPausedNodeIds((prev) => (prev.includes(nodeId) ? prev : [...prev, nodeId]))
|
||||
}, [])
|
||||
|
||||
const removeConnectionPathPausedNode = useCallback((nodeId: string) => {
|
||||
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(
|
||||
() => ({
|
||||
nodes,
|
||||
@@ -566,6 +613,14 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
connectionPathTriggerNodeIds,
|
||||
addConnectionPathTrigger,
|
||||
connectionPathNodeIds,
|
||||
connectionPathPausedSegmentNodeIds,
|
||||
connectionPathActiveSegmentNodeIds,
|
||||
connectionPathPausedNodeIds,
|
||||
addConnectionPathPausedNode,
|
||||
removeConnectionPathPausedNode,
|
||||
connectionPathErrorNodeIds,
|
||||
addConnectionPathError,
|
||||
removeConnectionPathError,
|
||||
startConnectionPathUpdate,
|
||||
endConnectionPathUpdate,
|
||||
}),
|
||||
@@ -586,6 +641,14 @@ export function CanvasPage({ projectId }: CanvasPageProps) {
|
||||
connectionPathTriggerNodeIds,
|
||||
addConnectionPathTrigger,
|
||||
connectionPathNodeIds,
|
||||
connectionPathPausedSegmentNodeIds,
|
||||
connectionPathActiveSegmentNodeIds,
|
||||
connectionPathPausedNodeIds,
|
||||
addConnectionPathPausedNode,
|
||||
removeConnectionPathPausedNode,
|
||||
connectionPathErrorNodeIds,
|
||||
addConnectionPathError,
|
||||
removeConnectionPathError,
|
||||
startConnectionPathUpdate,
|
||||
endConnectionPathUpdate,
|
||||
]
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
type EdgeProps,
|
||||
} from '@xyflow/react'
|
||||
import FlowContext from '../../lib/flowContext'
|
||||
import { getConnectionStatus, CONNECTION_STATUS_CLASS } from '../../lib/connectionStatus'
|
||||
import { getConnectionLabelForTarget } from '../../lib/nodeRegistry'
|
||||
|
||||
const EDGE_STROKE_WIDTH = 2
|
||||
@@ -28,6 +29,12 @@ export function AnimatedEdge({
|
||||
const ctx = useContext(FlowContext)
|
||||
const nodes = ctx?.nodes ?? []
|
||||
const pathNodeIds = ctx?.connectionPathNodeIds ?? EMPTY_PATH_NODE_IDS
|
||||
const pausedSegmentNodeIds = ctx?.connectionPathPausedSegmentNodeIds ?? 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 derivedLabel = useMemo(
|
||||
() => (targetNode?.type != null ? getConnectionLabelForTarget(targetNode.type) : undefined),
|
||||
@@ -35,7 +42,26 @@ export function AnimatedEdge({
|
||||
)
|
||||
const label = labelProp ?? derivedLabel
|
||||
|
||||
const isOnUpdatingPath = pathNodeIds.has(source) && pathNodeIds.has(target)
|
||||
const connectionStatus = useMemo(
|
||||
() =>
|
||||
getConnectionStatus({
|
||||
source,
|
||||
target,
|
||||
pathNodeIds,
|
||||
pausedSegmentNodeIds,
|
||||
activeSegmentNodeIds,
|
||||
errorTargetNodeIds,
|
||||
}),
|
||||
[
|
||||
source,
|
||||
target,
|
||||
pathNodeIds,
|
||||
pausedSegmentNodeIds,
|
||||
activeSegmentNodeIds,
|
||||
errorTargetNodeIds,
|
||||
]
|
||||
)
|
||||
const statusClass = CONNECTION_STATUS_CLASS[connectionStatus]
|
||||
|
||||
const [edgePath, edgeLabelX, edgeLabelY] = getBezierPath({
|
||||
sourceX,
|
||||
@@ -93,7 +119,7 @@ export function AnimatedEdge({
|
||||
strokeWidth: EDGE_STROKE_WIDTH,
|
||||
...style,
|
||||
}}
|
||||
className={`animated-edge-path${isOnUpdatingPath ? ' animated-edge-path--updating' : ''}`}
|
||||
className={`animated-edge-path${statusClass ? ` ${statusClass}` : ''}`}
|
||||
interactionWidth={interactionWidth}
|
||||
/>
|
||||
{label != null && (
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useCallback, useContext, useMemo, useState } from 'react'
|
||||
import React, { useCallback, useContext, useEffect, useMemo, useState } from 'react'
|
||||
import {
|
||||
AbstractNodeProps,
|
||||
createAbstractNodeComponent,
|
||||
@@ -26,6 +26,8 @@ export type AgentNodeData = {
|
||||
outputMarkdown?: string
|
||||
error?: string
|
||||
loading?: boolean
|
||||
/** Signature of inputs (sourceIds + config contents) from the last successful run. Used to show on-hold when current inputs differ. */
|
||||
lastRunSourceSignature?: string
|
||||
}
|
||||
|
||||
type Props = AbstractNodeProps<AgentNodeData>
|
||||
@@ -57,6 +59,8 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
||||
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
||||
const addConnectionPathPausedNode = flowContext?.addConnectionPathPausedNode
|
||||
const removeConnectionPathPausedNode = flowContext?.removeConnectionPathPausedNode
|
||||
const supportsFullscreen = getNodeType('agent')?.supportsFullscreen
|
||||
const { nodes, sourceIds, updateData } = useAbstractNode<AgentNodeData>(id, data ?? {})
|
||||
const { aiConnection } = usePlatform()
|
||||
@@ -79,6 +83,16 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
})
|
||||
}, [sourceIds, nodes])
|
||||
|
||||
const sourceSignature = useMemo(() => {
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
const n = nodes.find((n: { id: string }) => n.id === sid)
|
||||
return (n as { type?: string } | undefined)?.type === 'config'
|
||||
})
|
||||
.map((sid) => getConfigContent((nodes.find((n: { id: string }) => n.id === sid)?.data ?? undefined) as Record<string, unknown> | undefined))
|
||||
return JSON.stringify({ sourceIds: sourceIds.slice().sort(), configContents })
|
||||
}, [sourceIds, nodes])
|
||||
|
||||
const runAgent = useCallback(async () => {
|
||||
const configContents = sourceIds
|
||||
.filter((sid) => {
|
||||
@@ -89,6 +103,7 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const prompt = configContents.length > 0 ? configContents.join('\n\n---\n\n') : 'No prompt provided. Please describe what you want in structured markdown.'
|
||||
const contextNodes = sourceIds.map((sid) => ({ id: sid, content: serializeNodeForContext(nodes, sid) }))
|
||||
|
||||
removeConnectionPathPausedNode?.(id)
|
||||
updateData({ error: undefined, loading: true })
|
||||
startConnectionPathUpdate?.(id)
|
||||
setRunning(true)
|
||||
@@ -114,7 +129,12 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
return
|
||||
}
|
||||
const markdown = (json as { markdown?: string }).markdown ?? ''
|
||||
updateData({ loading: false, error: undefined, outputMarkdown: markdown })
|
||||
updateData({
|
||||
loading: false,
|
||||
error: undefined,
|
||||
outputMarkdown: markdown,
|
||||
lastRunSourceSignature: sourceSignature,
|
||||
})
|
||||
endConnectionPathUpdate?.(id)
|
||||
} catch (err: unknown) {
|
||||
updateData({
|
||||
@@ -126,17 +146,31 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
} finally {
|
||||
setRunning(false)
|
||||
}
|
||||
}, [sourceIds, nodes, contextText, updateData, aiConnection])
|
||||
}, [sourceIds, nodes, contextText, sourceSignature, updateData, aiConnection, removeConnectionPathPausedNode, startConnectionPathUpdate, endConnectionPathUpdate])
|
||||
|
||||
const onContextChange = useCallback(
|
||||
(e: React.ChangeEvent<HTMLTextAreaElement>) => updateData({ context: e.target.value }),
|
||||
[updateData]
|
||||
)
|
||||
|
||||
const pathNodeIds = flowContext?.connectionPathNodeIds
|
||||
const triggerNodeIds = flowContext?.connectionPathTriggerNodeIds ?? []
|
||||
const lastRunSourceSignature = data?.lastRunSourceSignature
|
||||
const hasPendingInputs =
|
||||
pathNodeIds?.has(id) &&
|
||||
triggerNodeIds.length > 0 &&
|
||||
!loading &&
|
||||
sourceSignature !== lastRunSourceSignature
|
||||
useEffect(() => {
|
||||
if (hasPendingInputs) addConnectionPathPausedNode?.(id)
|
||||
else removeConnectionPathPausedNode?.(id)
|
||||
}, [id, hasPendingInputs, addConnectionPathPausedNode, removeConnectionPathPausedNode])
|
||||
|
||||
return (
|
||||
<BaseNode
|
||||
className="min-w-[360px] min-h-[320px]"
|
||||
dimensions={dimensions}
|
||||
resizable
|
||||
nodeId={id}
|
||||
selected={selected}
|
||||
handles={
|
||||
@@ -149,6 +183,23 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
<BaseNodeHeaderRow
|
||||
icon={<Bot className="size-4" />}
|
||||
title={<NodeHeaderTitle nodeId={id} displayTitle={id} />}
|
||||
right={
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="shrink-0 h-7 nodrag nopan"
|
||||
onClick={runAgent}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running || loading ? (
|
||||
<Loader2 className="size-3.5 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-3.5" />
|
||||
)}
|
||||
<span className="ml-1.5">Run</span>
|
||||
</Button>
|
||||
}
|
||||
onHeaderDoubleClick={supportsFullscreen && setFullscreenNodeId ? () => setFullscreenNodeId(id) : undefined}
|
||||
/>
|
||||
<BaseNodeContent>
|
||||
@@ -179,20 +230,6 @@ function AgentNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
</ul>
|
||||
)}
|
||||
</div>
|
||||
<Button
|
||||
type="button"
|
||||
size="sm"
|
||||
className="w-fit"
|
||||
onClick={runAgent}
|
||||
disabled={running || loading}
|
||||
>
|
||||
{running || loading ? (
|
||||
<Loader2 className="size-3.5 mr-1.5 animate-spin" />
|
||||
) : (
|
||||
<Play className="size-3.5 mr-1.5" />
|
||||
)}
|
||||
Run
|
||||
</Button>
|
||||
{error && (
|
||||
<p className="text-xs text-destructive">{error}</p>
|
||||
)}
|
||||
|
||||
@@ -52,6 +52,8 @@ function RenderingNodeComponent({ id, data, width, height, selected }: Props) {
|
||||
const setFullscreenNodeId = flowContext?.setFullscreenNodeId
|
||||
const startConnectionPathUpdate = flowContext?.startConnectionPathUpdate
|
||||
const endConnectionPathUpdate = flowContext?.endConnectionPathUpdate
|
||||
const addConnectionPathError = flowContext?.addConnectionPathError
|
||||
const removeConnectionPathError = flowContext?.removeConnectionPathError
|
||||
const supportsFullscreen = getNodeType('render')?.supportsFullscreen
|
||||
const [renderedContent, setRenderedContent] = 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 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) */
|
||||
const connectedNodeIds = useMemo(() => {
|
||||
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',
|
||||
}
|
||||
@@ -36,6 +36,22 @@ export type FlowContextValue = {
|
||||
addConnectionPathTrigger: (nodeId: string) => void
|
||||
/** All node ids on the path of an update. Edges with both endpoints in this set animate. */
|
||||
connectionPathNodeIds: Set<string>
|
||||
/** Path nodes in the "paused" segment (from trigger up to Archon on hold). Those edges are yellow. */
|
||||
connectionPathPausedSegmentNodeIds: Set<string>
|
||||
/** Path nodes not in the paused segment (downstream of pause). Only those edges are blue (updating). */
|
||||
connectionPathActiveSegmentNodeIds: Set<string>
|
||||
/** Archon-type nodes that are on hold (e.g. Agent waiting for Run). */
|
||||
connectionPathPausedNodeIds: string[]
|
||||
/** Add this node as paused (on hold); remove when user continues. */
|
||||
addConnectionPathPausedNode: (nodeId: string) => void
|
||||
/** Remove this node from paused. */
|
||||
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. */
|
||||
startConnectionPathUpdate: (nodeId: string) => void
|
||||
/** Call when a path update ends for this node. If min duration not reached, animation continues until then. */
|
||||
|
||||
@@ -40,28 +40,84 @@ export function getUpstreamNodeIds(edges: GraphEdge[], seedIds: string[]): Set<s
|
||||
|
||||
/**
|
||||
* All node ids that lie on the path of an update.
|
||||
* - If triggerNodeIds is non-empty: path = nodes that are both downstream of a trigger and
|
||||
* upstream of an updating node (only the chain that actually triggered the update).
|
||||
* - Otherwise: path = upstream ∪ updating ∪ downstream of updating (legacy full upstream/downstream).
|
||||
* An edge should show the update animation iff both its source and target are in this set.
|
||||
* - If updatingNodeIds non-empty: path = downstream(trigger) ∩ (upstream(updating) ∪ downstream(updating))
|
||||
* so we include config→agent→rendering when agent is running.
|
||||
* - Else if triggerNodeIds and pausedNodeIds non-empty: path = downstream(trigger) ∩ upstream(paused)
|
||||
* so we show yellow (config→agent) when config changed and agent is on hold.
|
||||
* An edge should show color iff both its source and target are in this set.
|
||||
*/
|
||||
export function getPathNodeIds(
|
||||
edges: GraphEdge[],
|
||||
updatingNodeIds: string[],
|
||||
triggerNodeIds?: string[]
|
||||
triggerNodeIds?: string[],
|
||||
pausedNodeIds?: string[]
|
||||
): Set<string> {
|
||||
if (updatingNodeIds.length === 0) return new Set()
|
||||
const upstream = getUpstreamNodeIds(edges, updatingNodeIds)
|
||||
if (triggerNodeIds != null && triggerNodeIds.length > 0) {
|
||||
const hasUpdating = updatingNodeIds.length > 0
|
||||
const hasPausedPath =
|
||||
pausedNodeIds != null &&
|
||||
pausedNodeIds.length > 0 &&
|
||||
triggerNodeIds != null &&
|
||||
triggerNodeIds.length > 0
|
||||
|
||||
if (hasUpdating && triggerNodeIds != null && triggerNodeIds.length > 0) {
|
||||
const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds)
|
||||
const upstreamOfUpdating = getUpstreamNodeIds(edges, updatingNodeIds)
|
||||
const downstreamOfUpdating = getDownstreamNodeIds(edges, updatingNodeIds)
|
||||
const path = new Set<string>()
|
||||
downstreamOfTrigger.forEach((id) => {
|
||||
if (upstreamOfUpdating.has(id) || downstreamOfUpdating.has(id)) path.add(id)
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
if (hasPausedPath && !hasUpdating) {
|
||||
const upstream = getUpstreamNodeIds(edges, pausedNodeIds!)
|
||||
const downstreamOfTrigger = getDownstreamNodeIds(edges, triggerNodeIds!)
|
||||
const path = new Set<string>()
|
||||
upstream.forEach((id) => {
|
||||
if (downstreamOfTrigger.has(id)) path.add(id)
|
||||
})
|
||||
return path
|
||||
}
|
||||
|
||||
if (hasUpdating) {
|
||||
const upstream = getUpstreamNodeIds(edges, updatingNodeIds)
|
||||
const downstream = getDownstreamNodeIds(edges, updatingNodeIds)
|
||||
const path = new Set<string>(upstream)
|
||||
downstream.forEach((id) => path.add(id))
|
||||
return path
|
||||
}
|
||||
|
||||
return new Set()
|
||||
}
|
||||
|
||||
/**
|
||||
* Path nodes from triggers up to and including the first paused node (Archon on hold).
|
||||
* Used to color those edges yellow; rest of path stays blue.
|
||||
*/
|
||||
export function getPausedSegmentNodeIds(
|
||||
edges: GraphEdge[],
|
||||
pathNodeIds: Set<string>,
|
||||
triggerNodeIds: string[],
|
||||
pausedNodeIds: string[]
|
||||
): Set<string> {
|
||||
if (pausedNodeIds.length === 0 || triggerNodeIds.length === 0) return new Set()
|
||||
const pausedSet = new Set(pausedNodeIds)
|
||||
const seeds = triggerNodeIds.filter((id) => pathNodeIds.has(id))
|
||||
if (seeds.length === 0) return new Set()
|
||||
const out = new Set<string>(seeds)
|
||||
const frontier: string[] = [...seeds]
|
||||
const visited = new Set<string>(seeds)
|
||||
while (frontier.length > 0) {
|
||||
const n = frontier.shift()!
|
||||
if (pausedSet.has(n)) continue
|
||||
for (const e of edges) {
|
||||
if (e.source !== n || !pathNodeIds.has(e.target) || visited.has(e.target)) continue
|
||||
visited.add(e.target)
|
||||
out.add(e.target)
|
||||
if (pausedSet.has(e.target)) continue
|
||||
frontier.push(e.target)
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
@@ -153,6 +153,19 @@ body {
|
||||
.react-flow__edge path.animated-edge-path.animated-edge-path--updating,
|
||||
.animated-edge-path.animated-edge-path--updating {
|
||||
stroke: hsl(217 91% 60%);
|
||||
transition: none;
|
||||
}
|
||||
|
||||
.react-flow__edge path.animated-edge-path.animated-edge-path--paused,
|
||||
.animated-edge-path.animated-edge-path--paused {
|
||||
stroke: hsl(45 98% 50%);
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user