import { useMemo } from 'react'; import { Node, Edge } from '@xyflow/react'; import { Feature } from '@/store/app-store'; import { createFeatureMap, getBlockingDependenciesFromMap } from '@automaker/dependency-resolver'; import { GRAPH_RENDER_MODE_FULL, type GraphRenderMode } from '../constants'; import { GraphFilterResult } from './use-graph-filter'; export interface TaskNodeData extends Feature { // Re-declare properties from BaseFeature that have index signature issues priority?: number; error?: string; branchName?: string; dependencies?: string[]; // Task node specific properties isBlocked: boolean; isRunning: boolean; blockingDependencies: string[]; // Filter highlight states isMatched?: boolean; isHighlighted?: boolean; isDimmed?: boolean; // Background/theme settings cardOpacity?: number; cardGlassmorphism?: boolean; cardBorderEnabled?: boolean; cardBorderOpacity?: number; // Action callbacks onViewLogs?: () => void; onViewDetails?: () => void; onStartTask?: () => void; onStopTask?: () => void; onResumeTask?: () => void; onSpawnTask?: () => void; onDeleteTask?: () => void; renderMode?: GraphRenderMode; } export type TaskNode = Node; export type DependencyEdge = Edge<{ sourceStatus: Feature['status']; targetStatus: Feature['status']; isHighlighted?: boolean; isDimmed?: boolean; onDeleteDependency?: (sourceId: string, targetId: string) => void; renderMode?: GraphRenderMode; }>; export interface NodeActionCallbacks { onViewLogs?: (featureId: string) => void; onViewDetails?: (featureId: string) => void; onStartTask?: (featureId: string) => void; onStopTask?: (featureId: string) => void; onResumeTask?: (featureId: string) => void; onSpawnTask?: (featureId: string) => void; onDeleteTask?: (featureId: string) => void; onDeleteDependency?: (sourceId: string, targetId: string) => void; } interface BackgroundSettings { cardOpacity: number; cardGlassmorphism: boolean; cardBorderEnabled: boolean; cardBorderOpacity: number; } interface UseGraphNodesProps { features: Feature[]; runningAutoTasks: string[]; filterResult?: GraphFilterResult; actionCallbacks?: NodeActionCallbacks; backgroundSettings?: BackgroundSettings; renderMode?: GraphRenderMode; enableEdgeAnimations?: boolean; } /** * Transforms features into React Flow nodes and edges * Creates dependency edges based on feature.dependencies array */ export function useGraphNodes({ features, runningAutoTasks, filterResult, actionCallbacks, backgroundSettings, renderMode = GRAPH_RENDER_MODE_FULL, enableEdgeAnimations = true, }: UseGraphNodesProps) { const { nodes, edges } = useMemo(() => { const nodeList: TaskNode[] = []; const edgeList: DependencyEdge[] = []; const featureMap = createFeatureMap(features); const runningTaskIds = new Set(runningAutoTasks); // Extract filter state const hasActiveFilter = filterResult?.hasActiveFilter ?? false; const matchedNodeIds = filterResult?.matchedNodeIds ?? new Set(); const highlightedNodeIds = filterResult?.highlightedNodeIds ?? new Set(); const highlightedEdgeIds = filterResult?.highlightedEdgeIds ?? new Set(); // Create nodes features.forEach((feature) => { const isRunning = runningTaskIds.has(feature.id); const blockingDeps = getBlockingDependenciesFromMap(feature, featureMap); // Calculate filter highlight states const isMatched = hasActiveFilter && matchedNodeIds.has(feature.id); const isHighlighted = hasActiveFilter && highlightedNodeIds.has(feature.id); const isDimmed = hasActiveFilter && !highlightedNodeIds.has(feature.id); const node: TaskNode = { id: feature.id, type: 'task', position: { x: 0, y: 0 }, // Will be set by layout data: { ...feature, isBlocked: blockingDeps.length > 0, isRunning, blockingDependencies: blockingDeps, // Filter states isMatched, isHighlighted, isDimmed, // Background/theme settings cardOpacity: backgroundSettings?.cardOpacity, cardGlassmorphism: backgroundSettings?.cardGlassmorphism, cardBorderEnabled: backgroundSettings?.cardBorderEnabled, cardBorderOpacity: backgroundSettings?.cardBorderOpacity, renderMode, // Action callbacks (bound to this feature's ID) onViewLogs: actionCallbacks?.onViewLogs ? () => actionCallbacks.onViewLogs!(feature.id) : undefined, onViewDetails: actionCallbacks?.onViewDetails ? () => actionCallbacks.onViewDetails!(feature.id) : undefined, onStartTask: actionCallbacks?.onStartTask ? () => actionCallbacks.onStartTask!(feature.id) : undefined, onStopTask: actionCallbacks?.onStopTask ? () => actionCallbacks.onStopTask!(feature.id) : undefined, onResumeTask: actionCallbacks?.onResumeTask ? () => actionCallbacks.onResumeTask!(feature.id) : undefined, onSpawnTask: actionCallbacks?.onSpawnTask ? () => actionCallbacks.onSpawnTask!(feature.id) : undefined, onDeleteTask: actionCallbacks?.onDeleteTask ? () => actionCallbacks.onDeleteTask!(feature.id) : undefined, }, }; nodeList.push(node); // Create edges for dependencies const deps = feature.dependencies as string[] | undefined; if (deps && deps.length > 0) { deps.forEach((depId: string) => { // Only create edge if the dependency exists in current view if (featureMap.has(depId)) { const sourceFeature = featureMap.get(depId)!; const edgeId = `${depId}->${feature.id}`; // Calculate edge highlight states const edgeIsHighlighted = hasActiveFilter && highlightedEdgeIds.has(edgeId); const edgeIsDimmed = hasActiveFilter && !highlightedEdgeIds.has(edgeId); const edge: DependencyEdge = { id: edgeId, source: depId, target: feature.id, type: 'dependency', animated: enableEdgeAnimations && (isRunning || runningTaskIds.has(depId)), data: { sourceStatus: sourceFeature.status as Feature['status'], targetStatus: feature.status, isHighlighted: edgeIsHighlighted, isDimmed: edgeIsDimmed, onDeleteDependency: actionCallbacks?.onDeleteDependency, renderMode, }, }; edgeList.push(edge); } }); } }); return { nodes: nodeList, edges: edgeList }; }, [features, runningAutoTasks, filterResult, actionCallbacks, backgroundSettings]); return { nodes, edges }; }