mirror of
https://github.com/microsoft/agent-framework.git
synced 2026-06-16 21:04:09 +08:00
Python: Add DevUI to AgentFramework (#781)
* add initial backend service code for devui * add tests * add frontendcode * ui updates * update readme * ui updates and tweaks * update ui bundle * improve ui, add react flow base * add react flow ui, fix background * update ui, fix introspection bug * update readme * update ui build * add support for multimodal input - both backend and frontend * update ui build * refactor as main framework package * backend and tests refactor * ui build update * ui build update and refactor * update pyproject.toml, update uv.lock * update ui build * ui update to fit oai responses types * add backend updat and readme update * mypy and other fixes * add intial dev guide * update ui and fix workflow bug * update ui build, add thread support * type fixes * update workflow view * update uv.lock * fix workflow iport errors * lint and other fixes * mypy fixes * minor update * update ui build * refactor to use oai dependencies directly, update examples to samples, improve typing * readme update * update ui and ui build * fix workflow pyright error * update ui, fix issues with run workflow placement, miniamp menu, etc * make samples integrate serve --------- Co-authored-by: Chris <66376200+crickman@users.noreply.github.com> Co-authored-by: Eric Zhu <ekzhu@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
Unverified
parent
adb6dcd2af
commit
1ef24d3e91
@@ -0,0 +1,139 @@
|
||||
import type { Node, Edge } from "@xyflow/react";
|
||||
import type { ExecutorNodeData } from "@/components/workflow/executor-node";
|
||||
|
||||
/**
|
||||
* Lightweight auto-layout algorithm to replace dagre
|
||||
* Handles fan-out nodes properly by spacing siblings
|
||||
*/
|
||||
export function applySimpleLayout(
|
||||
nodes: Node<ExecutorNodeData>[],
|
||||
edges: Edge[],
|
||||
direction: "TB" | "LR" = "LR"
|
||||
): Node<ExecutorNodeData>[] {
|
||||
if (nodes.length === 0) return nodes;
|
||||
if (nodes.length === 1) {
|
||||
return nodes.map((node) => ({
|
||||
...node,
|
||||
position: { x: 0, y: 0 },
|
||||
}));
|
||||
}
|
||||
|
||||
// Create adjacency maps
|
||||
const outgoingEdges = new Map<string, string[]>();
|
||||
const incomingEdges = new Map<string, string[]>();
|
||||
|
||||
nodes.forEach((node) => {
|
||||
outgoingEdges.set(node.id, []);
|
||||
incomingEdges.set(node.id, []);
|
||||
});
|
||||
|
||||
edges.forEach((edge) => {
|
||||
outgoingEdges.get(edge.source)?.push(edge.target);
|
||||
incomingEdges.get(edge.target)?.push(edge.source);
|
||||
});
|
||||
|
||||
// Find root nodes (nodes with no incoming edges)
|
||||
const rootNodes = nodes.filter(
|
||||
(node) => (incomingEdges.get(node.id) || []).length === 0
|
||||
);
|
||||
|
||||
if (rootNodes.length === 0) {
|
||||
// Fallback: use first node as root if no clear root
|
||||
rootNodes.push(nodes[0]);
|
||||
}
|
||||
|
||||
// Constants for spacing
|
||||
const NODE_WIDTH = 220;
|
||||
const NODE_HEIGHT = 120;
|
||||
const HORIZONTAL_SPACING = direction === "LR" ? 350 : 200;
|
||||
const VERTICAL_SPACING = direction === "TB" ? 250 : 180;
|
||||
|
||||
// Track positioned nodes and level information
|
||||
const positioned = new Map<string, { x: number; y: number; level: number }>();
|
||||
const levelGroups = new Map<number, string[]>();
|
||||
|
||||
// Build level groups using BFS
|
||||
const queue: Array<{ nodeId: string; level: number }> = [];
|
||||
const visited = new Set<string>();
|
||||
|
||||
// Start with root nodes at level 0
|
||||
rootNodes.forEach((node) => {
|
||||
queue.push({ nodeId: node.id, level: 0 });
|
||||
});
|
||||
|
||||
// BFS to assign levels
|
||||
while (queue.length > 0) {
|
||||
const { nodeId, level } = queue.shift()!;
|
||||
|
||||
if (visited.has(nodeId)) continue;
|
||||
visited.add(nodeId);
|
||||
|
||||
// Add to level group
|
||||
if (!levelGroups.has(level)) {
|
||||
levelGroups.set(level, []);
|
||||
}
|
||||
levelGroups.get(level)!.push(nodeId);
|
||||
|
||||
// Add children to next level
|
||||
const children = outgoingEdges.get(nodeId) || [];
|
||||
children.forEach((childId) => {
|
||||
if (!visited.has(childId)) {
|
||||
queue.push({ nodeId: childId, level: level + 1 });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Handle orphaned nodes (not connected to root)
|
||||
nodes.forEach((node) => {
|
||||
if (!visited.has(node.id)) {
|
||||
const maxLevel = Math.max(...Array.from(levelGroups.keys()), -1);
|
||||
const orphanLevel = maxLevel + 1;
|
||||
|
||||
if (!levelGroups.has(orphanLevel)) {
|
||||
levelGroups.set(orphanLevel, []);
|
||||
}
|
||||
levelGroups.get(orphanLevel)!.push(node.id);
|
||||
}
|
||||
});
|
||||
|
||||
// Position nodes level by level
|
||||
levelGroups.forEach((nodeIds, level) => {
|
||||
const nodeCount = nodeIds.length;
|
||||
|
||||
nodeIds.forEach((nodeId, index) => {
|
||||
let x: number, y: number;
|
||||
|
||||
if (direction === "LR") {
|
||||
// Horizontal layout: X increases with level, Y centers siblings
|
||||
x = level * HORIZONTAL_SPACING;
|
||||
|
||||
// Center siblings vertically
|
||||
const totalHeight = (nodeCount - 1) * VERTICAL_SPACING;
|
||||
const startY = -totalHeight / 2;
|
||||
y = startY + index * VERTICAL_SPACING;
|
||||
} else {
|
||||
// Vertical layout: Y increases with level, X centers siblings
|
||||
y = level * VERTICAL_SPACING;
|
||||
|
||||
// Center siblings horizontally
|
||||
const totalWidth = (nodeCount - 1) * HORIZONTAL_SPACING;
|
||||
const startX = -totalWidth / 2;
|
||||
x = startX + index * HORIZONTAL_SPACING;
|
||||
}
|
||||
|
||||
positioned.set(nodeId, { x, y, level });
|
||||
});
|
||||
});
|
||||
|
||||
// Apply positions to nodes (centering them on their calculated positions)
|
||||
return nodes.map((node) => {
|
||||
const pos = positioned.get(node.id) || { x: 0, y: 0 };
|
||||
return {
|
||||
...node,
|
||||
position: {
|
||||
x: pos.x - NODE_WIDTH / 2, // Center the node
|
||||
y: pos.y - NODE_HEIGHT / 2,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,515 @@
|
||||
import { applySimpleLayout } from "./simple-layout";
|
||||
import type { Node, Edge } from "@xyflow/react";
|
||||
import type {
|
||||
ExecutorNodeData,
|
||||
ExecutorState,
|
||||
} from "@/components/workflow/executor-node";
|
||||
import type {
|
||||
ExtendedResponseStreamEvent,
|
||||
ResponseWorkflowEventComplete,
|
||||
} from "@/types";
|
||||
import type { Workflow } from "@/types/workflow";
|
||||
import { getTypedWorkflow } from "@/types/workflow";
|
||||
|
||||
export interface WorkflowDumpExecutor {
|
||||
id: string;
|
||||
type: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface RawExecutorData {
|
||||
type_?: string;
|
||||
type?: string;
|
||||
name?: string;
|
||||
description?: string;
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface WorkflowDumpConnection {
|
||||
source: string;
|
||||
target: string;
|
||||
condition?: string;
|
||||
}
|
||||
|
||||
export interface WorkflowDump {
|
||||
executors?: WorkflowDumpExecutor[];
|
||||
connections?: WorkflowDumpConnection[];
|
||||
start_executor?: string;
|
||||
end_executors?: string[];
|
||||
[key: string]: unknown; // Allow for additional properties
|
||||
}
|
||||
|
||||
export interface NodeUpdate {
|
||||
nodeId: string;
|
||||
state: ExecutorState;
|
||||
data?: unknown;
|
||||
error?: string;
|
||||
timestamp: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert workflow dump data to React Flow nodes
|
||||
*/
|
||||
export function convertWorkflowDumpToNodes(
|
||||
workflowDump: Workflow | Record<string, unknown> | undefined,
|
||||
onNodeClick?: (executorId: string, data: ExecutorNodeData) => void
|
||||
): Node<ExecutorNodeData>[] {
|
||||
if (!workflowDump) {
|
||||
console.warn("convertWorkflowDumpToNodes: workflowDump is undefined");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to get typed workflow first, then fall back to generic handling
|
||||
const typedWorkflow = getTypedWorkflow(workflowDump);
|
||||
|
||||
let executors: WorkflowDumpExecutor[];
|
||||
let startExecutorId: string | undefined;
|
||||
|
||||
if (typedWorkflow) {
|
||||
// Use typed workflow structure
|
||||
executors = Object.values(typedWorkflow.executors).map((executor) => ({
|
||||
id: executor.id,
|
||||
type: executor.type,
|
||||
name:
|
||||
((executor as Record<string, unknown>).name as string) || executor.id,
|
||||
description: (executor as Record<string, unknown>).description as string,
|
||||
config: (executor as Record<string, unknown>).config as Record<
|
||||
string,
|
||||
unknown
|
||||
>,
|
||||
}));
|
||||
startExecutorId = typedWorkflow.start_executor_id;
|
||||
} else {
|
||||
// Fall back to generic handling for backwards compatibility
|
||||
executors = getExecutorsFromDump(workflowDump as Record<string, unknown>);
|
||||
const workflowDumpRecord = workflowDump as Record<string, unknown>;
|
||||
startExecutorId = workflowDumpRecord?.start_executor_id as
|
||||
| string
|
||||
| undefined;
|
||||
}
|
||||
|
||||
if (!executors || !Array.isArray(executors) || executors.length === 0) {
|
||||
console.warn(
|
||||
"No executors found in workflow dump. Available keys:",
|
||||
Object.keys(workflowDump)
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const nodes = executors.map((executor) => ({
|
||||
id: executor.id,
|
||||
type: "executor",
|
||||
position: { x: 0, y: 0 }, // Will be set by layout algorithm
|
||||
data: {
|
||||
executorId: executor.id,
|
||||
executorType: executor.type,
|
||||
name: executor.name || executor.id,
|
||||
state: "pending" as ExecutorState,
|
||||
isStartNode: executor.id === startExecutorId,
|
||||
onNodeClick,
|
||||
},
|
||||
}));
|
||||
|
||||
return nodes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert workflow dump data to React Flow edges
|
||||
*/
|
||||
export function convertWorkflowDumpToEdges(
|
||||
workflowDump: Workflow | Record<string, unknown> | undefined
|
||||
): Edge[] {
|
||||
if (!workflowDump) {
|
||||
console.warn("convertWorkflowDumpToEdges: workflowDump is undefined");
|
||||
return [];
|
||||
}
|
||||
|
||||
// Try to get typed workflow first, then fall back to generic handling
|
||||
const typedWorkflow = getTypedWorkflow(workflowDump);
|
||||
|
||||
let connections: WorkflowDumpConnection[];
|
||||
|
||||
if (typedWorkflow) {
|
||||
// Use typed workflow structure to extract connections from edge_groups
|
||||
connections = [];
|
||||
typedWorkflow.edge_groups.forEach((group) => {
|
||||
group.edges.forEach((edge) => {
|
||||
connections.push({
|
||||
source: edge.source_id,
|
||||
target: edge.target_id,
|
||||
condition: edge.condition_name,
|
||||
});
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Fall back to generic handling for backwards compatibility
|
||||
connections = getConnectionsFromDump(
|
||||
workflowDump as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
|
||||
if (!connections || !Array.isArray(connections) || connections.length === 0) {
|
||||
console.warn(
|
||||
"No connections found in workflow dump. Available keys:",
|
||||
Object.keys(workflowDump)
|
||||
);
|
||||
return [];
|
||||
}
|
||||
|
||||
const edges = connections.map((connection) => ({
|
||||
id: `${connection.source}-${connection.target}`,
|
||||
source: connection.source,
|
||||
target: connection.target,
|
||||
type: "default",
|
||||
animated: false,
|
||||
style: {
|
||||
stroke: "#6b7280",
|
||||
strokeWidth: 2,
|
||||
},
|
||||
}));
|
||||
|
||||
return edges;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract executors from workflow dump - handles different possible structures
|
||||
*/
|
||||
function getExecutorsFromDump(
|
||||
workflowDump: Record<string, unknown>
|
||||
): WorkflowDumpExecutor[] {
|
||||
// First check if executors is an object (like in the actual dump structure)
|
||||
if (
|
||||
workflowDump.executors &&
|
||||
typeof workflowDump.executors === "object" &&
|
||||
!Array.isArray(workflowDump.executors)
|
||||
) {
|
||||
const executorsObj = workflowDump.executors as Record<
|
||||
string,
|
||||
RawExecutorData
|
||||
>;
|
||||
return Object.entries(executorsObj).map(([id, executor]) => ({
|
||||
id,
|
||||
type: executor.type_ || executor.type || "executor",
|
||||
name: executor.name || id,
|
||||
description: executor.description,
|
||||
config: executor.config,
|
||||
}));
|
||||
}
|
||||
|
||||
// Try different possible keys where executors might be stored as arrays
|
||||
const possibleKeys = ["executors", "agents", "steps", "nodes"];
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
if (workflowDump[key] && Array.isArray(workflowDump[key])) {
|
||||
return workflowDump[key] as WorkflowDumpExecutor[];
|
||||
}
|
||||
}
|
||||
|
||||
// If no direct array, try to extract from nested structures
|
||||
if (workflowDump.config && typeof workflowDump.config === "object") {
|
||||
return getExecutorsFromDump(workflowDump.config as Record<string, unknown>);
|
||||
}
|
||||
|
||||
// Fallback: create executors from any object keys that look like executor IDs
|
||||
const executors: WorkflowDumpExecutor[] = [];
|
||||
Object.entries(workflowDump).forEach(([key, value]) => {
|
||||
if (
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
("type" in value || "type_" in value)
|
||||
) {
|
||||
const rawExecutor = value as RawExecutorData;
|
||||
executors.push({
|
||||
id: key,
|
||||
type: rawExecutor.type_ || rawExecutor.type || "executor",
|
||||
name: rawExecutor.name || key,
|
||||
description: rawExecutor.description,
|
||||
config: rawExecutor.config,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return executors;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract connections from workflow dump - handles different possible structures
|
||||
*/
|
||||
function getConnectionsFromDump(
|
||||
workflowDump: Record<string, unknown>
|
||||
): WorkflowDumpConnection[] {
|
||||
// Handle edge_groups structure (actual dump format)
|
||||
if (workflowDump.edge_groups && Array.isArray(workflowDump.edge_groups)) {
|
||||
const connections: WorkflowDumpConnection[] = [];
|
||||
workflowDump.edge_groups.forEach((group: unknown) => {
|
||||
if (typeof group === "object" && group !== null && "edges" in group) {
|
||||
const edges = (group as { edges: unknown }).edges;
|
||||
if (Array.isArray(edges)) {
|
||||
edges.forEach((edge: unknown) => {
|
||||
if (
|
||||
typeof edge === "object" &&
|
||||
edge !== null &&
|
||||
"source_id" in edge &&
|
||||
"target_id" in edge
|
||||
) {
|
||||
const edgeObj = edge as {
|
||||
source_id: string;
|
||||
target_id: string;
|
||||
condition_name?: string;
|
||||
};
|
||||
connections.push({
|
||||
source: edgeObj.source_id,
|
||||
target: edgeObj.target_id,
|
||||
condition: edgeObj.condition_name || undefined,
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
return connections;
|
||||
}
|
||||
|
||||
// Try different possible keys where connections might be stored
|
||||
const possibleKeys = ["connections", "edges", "transitions", "links"];
|
||||
|
||||
for (const key of possibleKeys) {
|
||||
if (workflowDump[key] && Array.isArray(workflowDump[key])) {
|
||||
return workflowDump[key] as WorkflowDumpConnection[];
|
||||
}
|
||||
}
|
||||
|
||||
// If no direct array, try to extract from nested structures
|
||||
if (workflowDump.config && typeof workflowDump.config === "object") {
|
||||
return getConnectionsFromDump(
|
||||
workflowDump.config as Record<string, unknown>
|
||||
);
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply auto-layout to nodes using a lightweight algorithm
|
||||
* Replaces dagre to eliminate 4.88MB lodash dependency
|
||||
*/
|
||||
export function applyDagreLayout(
|
||||
nodes: Node<ExecutorNodeData>[],
|
||||
edges: Edge[],
|
||||
direction: "TB" | "LR" = "LR"
|
||||
): Node<ExecutorNodeData>[] {
|
||||
return applySimpleLayout(nodes, edges, direction);
|
||||
}
|
||||
|
||||
/**
|
||||
* Process workflow events and extract node updates
|
||||
*/
|
||||
export function processWorkflowEvents(
|
||||
events: ExtendedResponseStreamEvent[]
|
||||
): Record<string, NodeUpdate> {
|
||||
const nodeUpdates: Record<string, NodeUpdate> = {};
|
||||
|
||||
events.forEach((event) => {
|
||||
if (
|
||||
event.type === "response.workflow_event.complete" &&
|
||||
"data" in event &&
|
||||
event.data
|
||||
) {
|
||||
const workflowEvent = event as ResponseWorkflowEventComplete;
|
||||
const data = workflowEvent.data;
|
||||
const executorId = data.executor_id;
|
||||
const eventType = data.event_type;
|
||||
const eventData = data.data;
|
||||
|
||||
let state: ExecutorState = "pending";
|
||||
let error: string | undefined;
|
||||
|
||||
// Map event types to executor states
|
||||
if (eventType === "ExecutorInvokedEvent") {
|
||||
state = "running";
|
||||
} else if (eventType === "ExecutorCompletedEvent") {
|
||||
state = "completed";
|
||||
} else if (
|
||||
eventType?.includes("Error") ||
|
||||
eventType?.includes("Failed")
|
||||
) {
|
||||
state = "failed";
|
||||
error = typeof eventData === "string" ? eventData : "Execution failed";
|
||||
} else if (eventType?.includes("Cancel")) {
|
||||
state = "cancelled";
|
||||
} else if (eventType === "WorkflowCompletedEvent") {
|
||||
state = "completed";
|
||||
}
|
||||
|
||||
// Update the node state (keep most recent update per executor)
|
||||
if (executorId) {
|
||||
nodeUpdates[executorId] = {
|
||||
nodeId: executorId,
|
||||
state,
|
||||
data: eventData,
|
||||
error,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return nodeUpdates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update node states based on event processing
|
||||
*/
|
||||
export function updateNodesWithEvents(
|
||||
nodes: Node<ExecutorNodeData>[],
|
||||
nodeUpdates: Record<string, NodeUpdate>
|
||||
): Node<ExecutorNodeData>[] {
|
||||
return nodes.map((node) => {
|
||||
const update = nodeUpdates[node.id];
|
||||
if (update) {
|
||||
return {
|
||||
...node,
|
||||
data: {
|
||||
...node.data,
|
||||
state: update.state,
|
||||
outputData: update.data,
|
||||
error: update.error,
|
||||
},
|
||||
};
|
||||
}
|
||||
return node;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get executors that are currently in execution (invoked but not yet completed)
|
||||
*/
|
||||
export function getCurrentlyExecutingExecutors(
|
||||
events: ExtendedResponseStreamEvent[]
|
||||
): string[] {
|
||||
const executorTimeline: Record<
|
||||
string,
|
||||
{ lastEvent: string; timestamp: string }
|
||||
> = {};
|
||||
|
||||
// Process events to find the most recent event for each executor
|
||||
events.forEach((event) => {
|
||||
if (
|
||||
event.type === "response.workflow_event.complete" &&
|
||||
"data" in event &&
|
||||
event.data
|
||||
) {
|
||||
const workflowEvent = event as ResponseWorkflowEventComplete;
|
||||
const data = workflowEvent.data;
|
||||
const executorId = data.executor_id;
|
||||
const eventType = data.event_type;
|
||||
|
||||
if (
|
||||
executorId &&
|
||||
(eventType === "ExecutorInvokedEvent" ||
|
||||
eventType === "ExecutorCompletedEvent")
|
||||
) {
|
||||
executorTimeline[executorId] = {
|
||||
lastEvent: eventType,
|
||||
timestamp: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Find executors that were invoked but haven't completed yet
|
||||
const currentlyExecuting = Object.entries(executorTimeline)
|
||||
.filter(([, timeline]) => timeline.lastEvent === "ExecutorInvokedEvent")
|
||||
.map(([executorId]) => executorId);
|
||||
|
||||
return currentlyExecuting;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update edges with sequence-based animation
|
||||
*/
|
||||
export function updateEdgesWithSequenceAnalysis(
|
||||
edges: Edge[],
|
||||
events: ExtendedResponseStreamEvent[]
|
||||
): Edge[] {
|
||||
const currentlyExecuting = getCurrentlyExecutingExecutors(events);
|
||||
|
||||
// Build simple state tracking for each executor
|
||||
const executorStates: Record<
|
||||
string,
|
||||
{ completed: boolean; invoked: boolean }
|
||||
> = {};
|
||||
|
||||
events.forEach((event) => {
|
||||
if (
|
||||
event.type === "response.workflow_event.complete" &&
|
||||
"data" in event &&
|
||||
event.data
|
||||
) {
|
||||
const workflowEvent = event as ResponseWorkflowEventComplete;
|
||||
const data = workflowEvent.data;
|
||||
const executorId = data.executor_id;
|
||||
const eventType = data.event_type;
|
||||
|
||||
if (executorId && eventType) {
|
||||
if (!executorStates[executorId]) {
|
||||
executorStates[executorId] = { completed: false, invoked: false };
|
||||
}
|
||||
|
||||
if (eventType === "ExecutorInvokedEvent") {
|
||||
executorStates[executorId].invoked = true;
|
||||
} else if (eventType === "ExecutorCompletedEvent") {
|
||||
executorStates[executorId].completed = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return edges.map((edge) => {
|
||||
const sourceState = executorStates[edge.source];
|
||||
const targetState = executorStates[edge.target];
|
||||
const targetIsExecuting = currentlyExecuting.includes(edge.target);
|
||||
|
||||
let style = { ...edge.style };
|
||||
let animated = false;
|
||||
|
||||
// Active edge: source completed and target is currently executing
|
||||
if (sourceState?.completed && targetIsExecuting) {
|
||||
style = {
|
||||
stroke: "#3b82f6", // Blue
|
||||
strokeWidth: 3,
|
||||
strokeDasharray: "5,5",
|
||||
};
|
||||
animated = true;
|
||||
}
|
||||
// Completed edge: both source and target have completed
|
||||
else if (sourceState?.completed && targetState?.completed) {
|
||||
style = {
|
||||
stroke: "#10b981", // Green
|
||||
strokeWidth: 2,
|
||||
};
|
||||
}
|
||||
// Invoked edge: source completed and target invoked (but not necessarily executing)
|
||||
else if (sourceState?.completed && targetState?.invoked) {
|
||||
style = {
|
||||
stroke: "#f59e0b", // Orange
|
||||
strokeWidth: 2,
|
||||
};
|
||||
}
|
||||
// Default: Not traversed
|
||||
else {
|
||||
style = {
|
||||
stroke: "#6b7280", // Gray
|
||||
strokeWidth: 2,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...edge,
|
||||
style,
|
||||
animated,
|
||||
};
|
||||
});
|
||||
}
|
||||
Reference in New Issue
Block a user