From e9dfc99d1ecd2ec699e98ab91d805c1065e9c4fb Mon Sep 17 00:00:00 2001 From: MayaCrmi Date: Tue, 24 Feb 2026 02:35:41 -0500 Subject: [PATCH 01/10] aligned with new code --- .../components/agentic-ai/AgentFlowGraph.tsx | 4 + .../components/agentic-ai/WorkflowsPanel.tsx | 25 +- .../agentic-ai/graphs/GraphCanvas.tsx | 3 + .../agentic-ai/graphs/GraphHeader.tsx | 6 +- .../agentic-ai/graphs/SaveBlueprintModal.tsx | 33 +- ui/client/src/hooks/use-graph-logic.ts | 118 +++-- ui/client/src/hooks/use-load-blueprint.ts | 424 ++++++++++++++++++ ui/client/src/pages/AgenticWorkflows.tsx | 23 +- ui/client/src/workspace/NewGraph.tsx | 12 +- 9 files changed, 586 insertions(+), 62 deletions(-) create mode 100644 ui/client/src/hooks/use-load-blueprint.ts diff --git a/ui/client/src/components/agentic-ai/AgentFlowGraph.tsx b/ui/client/src/components/agentic-ai/AgentFlowGraph.tsx index 830c19a30..2f561f8a2 100644 --- a/ui/client/src/components/agentic-ai/AgentFlowGraph.tsx +++ b/ui/client/src/components/agentic-ai/AgentFlowGraph.tsx @@ -9,12 +9,14 @@ type AgentFlowGraphProps = { selectedFlow: FlowObject | null; setSelectedFlow: (flow: FlowObject | null) => void; onValidationChange?: (isValid: boolean, validationResult: BlueprintValidationResult | null, isValidating: boolean) => void; + onFlowEdit?: (flow: FlowObject) => void; }; export default function AgentFlowGraph({ selectedFlow, setSelectedFlow, onValidationChange, + onFlowEdit, }: AgentFlowGraphProps): React.ReactElement { const handleFlowSelect = (flow: FlowObject | null): void => { @@ -41,9 +43,11 @@ export default function AgentFlowGraph({ selectedFlow={selectedFlow} onFlowSelect={handleFlowSelect} onFlowDelete={handleFlowDelete} + onFlowEdit={onFlowEdit} onValidationChange={onValidationChange} showActiveStatus={true} showDeleteButton={true} + showEditButton={true} height="100%" graphProps={{ showBackground: true, diff --git a/ui/client/src/components/agentic-ai/WorkflowsPanel.tsx b/ui/client/src/components/agentic-ai/WorkflowsPanel.tsx index ec4886e2e..df61220fb 100644 --- a/ui/client/src/components/agentic-ai/WorkflowsPanel.tsx +++ b/ui/client/src/components/agentic-ai/WorkflowsPanel.tsx @@ -1,6 +1,6 @@ import React, { useState, useEffect, useCallback } from "react"; import { motion } from "framer-motion"; -import { Trash2, Users } from "lucide-react"; +import { Trash2, Users, Pencil } from "lucide-react"; import { useAuth } from "@/contexts/AuthContext"; import { useShared } from "@/contexts/SharedContext"; import { Button } from "@/components/ui/button"; @@ -26,9 +26,11 @@ export interface WorkflowsPanelProps { selectedFlow: FlowObject | null; onFlowSelect: (flow: FlowObject | null) => void; onFlowDelete?: (flow: FlowObject) => void; + onFlowEdit?: (flow: FlowObject) => void; onValidationChange?: (isValid: boolean, validationResult: BlueprintValidationResult | null, isValidating: boolean) => void; showActiveStatus?: boolean; showDeleteButton?: boolean; + showEditButton?: boolean; className?: string; height?: string; graphProps?: { @@ -41,9 +43,11 @@ export default function WorkflowsPanel({ selectedFlow, onFlowSelect, onFlowDelete, + onFlowEdit, onValidationChange, showActiveStatus = false, showDeleteButton = false, + showEditButton = false, className = "", height = "100%", graphProps = { @@ -193,6 +197,13 @@ export default function WorkflowsPanel({ setShowDeleteModal(true); }; + const handleEditClick = (flow: FlowObject, event: React.MouseEvent) => { + event.stopPropagation(); + if (onFlowEdit) { + onFlowEdit(flow); + } + }; + const handleShareClick = (flow: FlowObject, event: React.MouseEvent) => { event.stopPropagation(); // Prevent flow selection when clicking share openShareForItem({ @@ -302,6 +313,18 @@ export default function WorkflowsPanel({ Active )} + {showEditButton && ( + Edit this workflow

}> + +
+ )} Share this workflow

}> diff --git a/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx b/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx index 1cefe8de9..91d78cc8b 100644 --- a/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx +++ b/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx @@ -1,5 +1,5 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Dialog, DialogContent, @@ -19,6 +19,9 @@ interface SaveBlueprintModalProps { onClose: () => void; onSave: (name: string, description: string) => void; isLoading?: boolean; + isEditMode?: boolean; + defaultName?: string; + defaultDescription?: string; } const SaveBlueprintModal: React.FC = ({ @@ -26,9 +29,19 @@ const SaveBlueprintModal: React.FC = ({ onClose, onSave, isLoading = false, + isEditMode = false, + defaultName = "", + defaultDescription = "", }) => { - const [name, setName] = useState(""); - const [description, setDescription] = useState(""); + const [name, setName] = useState(defaultName); + const [description, setDescription] = useState(defaultDescription); + + useEffect(() => { + if (isEditMode) { + setName(defaultName); + setDescription(defaultDescription); + } + }, [isEditMode, defaultName, defaultDescription]); const handleSave = () => { if (!name.trim()) { @@ -38,8 +51,8 @@ const SaveBlueprintModal: React.FC = ({ }; const handleClose = () => { - setName(""); - setDescription(""); + setName(isEditMode ? defaultName : ""); + setDescription(isEditMode ? defaultDescription : ""); onClose(); }; @@ -49,7 +62,9 @@ const SaveBlueprintModal: React.FC = ({ - Save Workflow + + {isEditMode ? "Update Workflow" : "Save Workflow"} +
@@ -59,7 +74,7 @@ const SaveBlueprintModal: React.FC = ({ setName(e.target.value)} className="input-dark-theme bg-input border-border text-foreground" @@ -103,10 +118,10 @@ const SaveBlueprintModal: React.FC = ({ {isLoading ? (
- Saving... + {isEditMode ? "Updating..." : "Saving..."}
) : ( - "Save Workflow" + isEditMode ? "Update Workflow" : "Save Workflow" )} diff --git a/ui/client/src/hooks/use-graph-logic.ts b/ui/client/src/hooks/use-graph-logic.ts index 0603ec324..284e30e49 100644 --- a/ui/client/src/hooks/use-graph-logic.ts +++ b/ui/client/src/hooks/use-graph-logic.ts @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo } from "react"; +import { useState, useCallback, useEffect, useMemo, useRef } from "react"; import { Node, Edge, @@ -16,37 +16,9 @@ import { useTheme } from "@/contexts/ThemeContext"; import { deriveThemeColors } from "@/lib/colorUtils"; import axios from "../http/axiosAgentConfig"; import * as yaml from "js-yaml"; -import { saveBlueprint } from "@/api/blueprints"; - -interface YamlFlowNode { - rid: string; - name: string; - type?: string; - config?: any; -} - -interface YamlFlowPlanStep { - uid: string; - node: string; - after?: string | string[] | null; - branches?: any; - exit_condition?: string; -} - -interface YamlFlowCondition { - rid: string; - name: string; - type?: string; - config?: any; -} - -interface YamlFlowState { - name?: string; - description?: string; - nodes: YamlFlowNode[]; - plan: YamlFlowPlanStep[]; - conditions?: YamlFlowCondition[]; -} +import { saveBlueprint, deleteBlueprint } from "@/api/blueprints"; +import { loadBlueprintForEditing } from "@/hooks/use-load-blueprint"; +import type { YamlFlowState } from "@/hooks/use-load-blueprint"; const defaulYmlState: YamlFlowState = { nodes: [ @@ -88,10 +60,12 @@ export interface SavedBlueprintInfo { interface UseGraphLogicOptions { /** Callback to execute after a successful save (e.g., navigate back to workflow list) */ onSaveComplete?: (savedBlueprint?: SavedBlueprintInfo) => void; + /** When provided, load this blueprint for editing instead of starting with an empty canvas */ + editBlueprintId?: string | null; } export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { - const { onSaveComplete } = options; + const { onSaveComplete, editBlueprintId } = options; const { toast } = useToast(); const [nodes, setNodes, onNodesChange] = useNodesState([]); const [edges, setEdges, onEdgesChange] = useEdgesState([]); @@ -151,6 +125,12 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { // Drag state to track what type of item is being dragged const [isDraggingCondition, setIsDraggingCondition] = useState(false); + // Edit mode state + const [isEditMode, setIsEditMode] = useState(!!editBlueprintId); + const [editBlueprintName, setEditBlueprintName] = useState(""); + const [editBlueprintDescription, setEditBlueprintDescription] = useState(""); + const blueprintLoadedRef = useRef(false); + const { user } = useAuth(); const USER_ID = user?.username || "default"; @@ -617,9 +597,52 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { useEffect(() => { loadBuildingBlocks(); - initializeDefaultNodes(); + if (!editBlueprintId) { + initializeDefaultNodes(); + } }, [loadBuildingBlocks]); + // Load existing blueprint for editing once building blocks are ready + useEffect(() => { + if (!editBlueprintId || isLoadingBlocks || blueprintLoadedRef.current) return; + blueprintLoadedRef.current = true; + + loadBlueprintForEditing( + editBlueprintId, + allBlocksData, + conditionsData, + conditionEdgeColor, + ) + .then((result) => { + const nodesWithCallbacks = result.nodes.map((node) => ({ + ...node, + data: { + ...node.data, + onDelete: deleteNode, + allBlocks: allBlocksData, + onAttachCondition: attachConditionToNode, + onRemoveCondition: removeConditionFromNode, + }, + })); + setNodes(nodesWithCallbacks); + setEdges(result.edges); + setYamlFlow(result.yamlFlow); + setNodeId(result.nextNodeId); + setIsEditMode(true); + setEditBlueprintName(result.name); + setEditBlueprintDescription(result.description); + }) + .catch((err) => { + console.error("Failed to load blueprint for editing:", err); + toast({ + title: "Failed to load blueprint", + description: "Could not load the blueprint for editing. Starting with a blank canvas.", + variant: "destructive", + }); + initializeDefaultNodes(); + }); + }, [editBlueprintId, isLoadingBlocks]); + // Trigger validation whenever yamlFlow changes useEffect(() => { // Only validate if yamlFlow has meaningful content @@ -1027,7 +1050,6 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { try { setIsSaving(true); - // Update yamlFlow with name and description const updatedYamlFlow = { ...yamlFlow, name: name, @@ -1036,7 +1058,6 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { setYamlFlow(updatedYamlFlow); - // Convert to YAML string using js-yaml library const yamlString = yaml.dump(updatedYamlFlow, { indent: 2, lineWidth: -1, @@ -1047,19 +1068,26 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { const response = await saveBlueprint(yamlString, USER_ID); if (response.status === "success") { - // Show success toast + // In edit mode, remove the old blueprint so we don't end up with duplicates + if (isEditMode && editBlueprintId) { + try { + await deleteBlueprint(editBlueprintId); + } catch (delErr) { + console.warn("Could not remove previous blueprint version:", delErr); + } + } + toast({ - title: "✅ Blueprint Saved Successfully", - description: `Blueprint "${name}" saved successfully`, + title: isEditMode + ? "✅ Blueprint Updated Successfully" + : "✅ Blueprint Saved Successfully", + description: `Blueprint "${name}" ${isEditMode ? "updated" : "saved"} successfully`, variant: "default", }); - // Close the save modal immediately & Stop the saving state setSaveModalOpen(false); setIsSaving(false); - // Call the onSaveComplete callback to navigate back (if provided) - // Pass the saved blueprint info so it can be selected in the workflow list if (onSaveComplete) { setTimeout(() => { onSaveComplete({ @@ -1082,7 +1110,7 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { setIsSaving(false); } }, - [yamlFlow, toast, onSaveComplete], + [yamlFlow, toast, onSaveComplete, isEditMode, editBlueprintId], ); useEffect(() => { @@ -1274,5 +1302,9 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { saveModalOpen, setSaveModalOpen, isSaving, + // Edit mode state + isEditMode, + editBlueprintName, + editBlueprintDescription, }; }; diff --git a/ui/client/src/hooks/use-load-blueprint.ts b/ui/client/src/hooks/use-load-blueprint.ts new file mode 100644 index 000000000..cf619fd28 --- /dev/null +++ b/ui/client/src/hooks/use-load-blueprint.ts @@ -0,0 +1,424 @@ +import { Node, Edge, MarkerType } from "reactflow"; +import { BuildingBlock } from "@/types/graph"; +import { getCategoryDisplay } from "@/components/shared/helpers"; +import { getBlueprintInfo } from "@/api/blueprints"; + +export interface YamlFlowNode { + rid: string; + name: string; + type?: string; + config?: any; +} + +export interface YamlFlowPlanStep { + uid: string; + node: string; + after?: string | string[] | null; + branches?: any; + exit_condition?: string; +} + +export interface YamlFlowCondition { + rid: string; + name: string; + type?: string; + config?: any; +} + +export interface YamlFlowState { + name?: string; + description?: string; + nodes: YamlFlowNode[]; + plan: YamlFlowPlanStep[]; + conditions?: YamlFlowCondition[]; +} + +export interface ReconstructedGraph { + nodes: Node[]; + edges: Edge[]; + yamlFlow: YamlFlowState; + nextNodeId: number; + name: string; + description: string; +} + +function stripRef(rid: string): string { + return rid.startsWith("$ref:") ? rid.slice(5) : rid; +} + +function findBlockByRid(rid: string, blocks: BuildingBlock[]): BuildingBlock | null { + const strippedRid = stripRef(rid); + return ( + blocks.find( + (b) => b.workspaceData?.rid === strippedRid || b.id === strippedRid, + ) || null + ); +} + +/** + * Compute a hierarchical layout for plan steps using topological levels, + * matching the display approach used by the workflow list (JointJS layout). + */ +function computeLayout( + plan: YamlFlowPlanStep[], + specNodes: YamlFlowNode[], +): Map { + const positions = new Map(); + + // Build node-type lookup from spec nodes + const nodeDefByRef = new Map(); + for (const n of specNodes) { + nodeDefByRef.set(stripRef(n.rid), n); + } + const typeByUid = new Map(); + for (const step of plan) { + const def = nodeDefByRef.get(step.node); + typeByUid.set(step.uid, def?.type || "custom_agent_node"); + } + + // Build predecessor map from `after` + `branches` + const predecessors: Record = {}; + for (const step of plan) { + if (!predecessors[step.uid]) predecessors[step.uid] = []; + if (step.after) { + const afters = Array.isArray(step.after) ? step.after : [step.after]; + predecessors[step.uid].push(...afters); + } + if (step.branches) { + for (const targetUid of Object.values(step.branches)) { + const tid = targetUid as string; + if (!predecessors[tid]) predecessors[tid] = []; + if (!predecessors[tid].includes(step.uid)) { + predecessors[tid].push(step.uid); + } + } + } + } + + // Assign levels via iterative BFS + const level: Record = {}; + + // Level 0: user_question_node + for (const step of plan) { + if (typeByUid.get(step.uid) === "user_question_node") { + level[step.uid] = 0; + } + } + + let changed = true; + let iterations = 0; + const maxIter = plan.length * plan.length + 1; + while (changed && iterations < maxIter) { + changed = false; + iterations++; + for (const step of plan) { + if (level[step.uid] !== undefined) continue; + if (typeByUid.get(step.uid) === "final_answer_node") continue; + const preds = predecessors[step.uid] || []; + if (preds.length === 0) { + level[step.uid] = 1; + changed = true; + } else if (preds.every((p) => level[p] !== undefined)) { + level[step.uid] = Math.max(...preds.map((p) => level[p])) + 1; + changed = true; + } + } + } + + // Fallback for any unresolved non-final nodes + const maxLvl = Math.max(0, ...Object.values(level).filter((v) => v !== undefined)); + for (const step of plan) { + if (level[step.uid] === undefined && typeByUid.get(step.uid) !== "final_answer_node") { + level[step.uid] = maxLvl + 1; + } + } + + // Final answer always last + const finalMaxLvl = Math.max(0, ...Object.values(level).filter((v) => v !== undefined)); + for (const step of plan) { + if (typeByUid.get(step.uid) === "final_answer_node") { + level[step.uid] = finalMaxLvl + 1; + } + } + + // Group by level + const nodesByLevel: Record = {}; + for (const step of plan) { + const l = level[step.uid] ?? 0; + if (!nodesByLevel[l]) nodesByLevel[l] = []; + nodesByLevel[l].push(step.uid); + } + + // Position nodes — spread siblings horizontally, levels vertically + const Y_SPACING = 200; + const X_SPACING = 300; + const X_CENTER = 400; + + for (const [lvl, uids] of Object.entries(nodesByLevel)) { + const l = Number(lvl); + const totalWidth = (uids.length - 1) * X_SPACING; + const startX = X_CENTER - totalWidth / 2; + uids.forEach((uid, index) => { + positions.set(uid, { x: startX + index * X_SPACING, y: 100 + l * Y_SPACING }); + }); + } + + return positions; +} + +/** + * Reconstruct a React Flow graph (nodes + edges) and yamlFlow state + * from a blueprint's spec_dict. + */ +export function reconstructBlueprintGraph( + specDict: any, + allBlocksData: BuildingBlock[], + conditionsData: BuildingBlock[], + conditionEdgeColor: string, +): ReconstructedGraph { + const name: string = specDict.name || "Untitled"; + const description: string = specDict.description || ""; + const specNodes: YamlFlowNode[] = specDict.nodes || []; + const plan: YamlFlowPlanStep[] = specDict.plan || []; + const specConditions: YamlFlowCondition[] = specDict.conditions || []; + + const yamlFlow: YamlFlowState = { + name, + description, + nodes: specNodes, + plan, + conditions: specConditions, + }; + + const nodeDefByRef = new Map(); + for (const nodeDef of specNodes) { + const rawRid = stripRef(nodeDef.rid); + nodeDefByRef.set(rawRid, nodeDef); + nodeDefByRef.set(nodeDef.rid, nodeDef); + } + + const positions = computeLayout(plan, specNodes); + + const reactFlowNodes: Node[] = []; + let maxNodeId = plan.length + 1; + + for (const step of plan) { + const position = positions.get(step.uid) || { x: 400, y: 200 }; + let node: Node; + + if (step.uid === "user_input") { + node = { + id: "user_input", + type: "custom", + position, + data: { + label: "User Input", + icon: getCategoryDisplay("nodes").icon, + color: "#4A90E2", + style: "bg-blue-800 text-white border", + description: "User question input node", + workspaceData: { + rid: "user_question", + name: "user_question", + category: "nodes", + type: "user_question_node", + config: { name: "User Input", type: "user_question_node" }, + version: 1, + created: new Date().toISOString(), + updated: new Date().toISOString(), + nested_refs: [], + }, + referencedConditions: [], + }, + }; + } else if (step.uid === "finalize") { + node = { + id: "finalize", + type: "custom", + position, + data: { + label: "Final Answer", + icon: getCategoryDisplay("nodes").icon, + color: "#50C878", + style: "bg-green-800 text-white border", + description: "Final answer output node", + workspaceData: { + rid: "final_answer", + name: "final_answer", + category: "nodes", + type: "final_answer_node", + config: { name: "Final Answer", type: "final_answer_node" }, + version: 1, + created: new Date().toISOString(), + updated: new Date().toISOString(), + nested_refs: [], + }, + referencedConditions: [], + }, + }; + } else { + const nodeDef = nodeDefByRef.get(step.node); + const block = findBlockByRid(step.node, allBlocksData); + + const label = block?.label || nodeDef?.name || step.node; + const category = block?.workspaceData?.category || "nodes"; + const color = block?.color || getCategoryDisplay(category).color; + + const idMatch = step.uid.match(/-(\d+)$/); + if (idMatch) { + const num = parseInt(idMatch[1]); + if (num >= maxNodeId) maxNodeId = num + 1; + } + + node = { + id: step.uid, + type: "custom", + position, + data: { + label, + icon: getCategoryDisplay(category).icon, + color, + style: "bg-gray-800 text-white border", + description: block?.description || `${category} - ${label}`, + workspaceData: block?.workspaceData || { + rid: step.node, + name: nodeDef?.name || step.node, + category: "nodes", + type: nodeDef?.type || "unknown", + config: nodeDef?.config || {}, + version: 1, + created: new Date().toISOString(), + updated: new Date().toISOString(), + nested_refs: [], + }, + referencedConditions: [], + }, + }; + } + + // Attach conditions from the plan step + if (step.exit_condition) { + const condBlock = findBlockByRid(step.exit_condition, conditionsData); + const condDef = specConditions.find( + (c) => stripRef(c.rid) === step.exit_condition, + ); + + if (condBlock) { + node.data.referencedConditions = [condBlock]; + } else if (condDef) { + node.data.referencedConditions = [ + { + id: stripRef(condDef.rid), + type: condDef.type || "unknown", + label: condDef.name, + color: getCategoryDisplay("conditions").color, + description: `conditions/${condDef.type} - ${condDef.name}`, + workspaceData: { + rid: stripRef(condDef.rid), + name: condDef.name, + category: "conditions", + type: condDef.type || "unknown", + config: condDef.config || {}, + version: 1, + created: new Date().toISOString(), + updated: new Date().toISOString(), + nested_refs: [], + }, + }, + ]; + } + } + + reactFlowNodes.push(node); + } + + // Reconstruct edges + const reactFlowEdges: Edge[] = []; + + // Track source→target pairs covered by branches to avoid duplicates + const branchPairs = new Set(); + for (const step of plan) { + if (step.branches) { + for (const targetUid of Object.values(step.branches)) { + branchPairs.add(`${step.uid}->${targetUid}`); + } + } + } + + // Regular edges from "after" fields + for (const step of plan) { + if (step.after) { + const afterList = Array.isArray(step.after) + ? step.after + : [step.after]; + for (const afterUid of afterList) { + if (!branchPairs.has(`${afterUid}->${step.uid}`)) { + reactFlowEdges.push({ + id: `${afterUid}-${step.uid}`, + source: afterUid, + target: step.uid, + type: "custom", + style: { strokeWidth: 2 }, + markerEnd: { + type: MarkerType.ArrowClosed, + width: 20, + height: 20, + }, + }); + } + } + } + } + + // Conditional edges from "branches" fields + for (const step of plan) { + if (step.branches) { + for (const [branch, targetUid] of Object.entries(step.branches)) { + reactFlowEdges.push({ + id: `${step.uid}-${targetUid}-${branch}`, + source: step.uid, + target: targetUid as string, + type: "custom", + style: { strokeDasharray: "5,5", stroke: conditionEdgeColor }, + markerEnd: { + type: MarkerType.ArrowClosed, + color: conditionEdgeColor, + }, + data: { + branch: String(branch), + isConditional: true, + }, + label: String(branch), + }); + } + } + } + + return { + nodes: reactFlowNodes, + edges: reactFlowEdges, + yamlFlow, + nextNodeId: maxNodeId, + name, + description, + }; +} + +/** + * Fetch a blueprint by ID and reconstruct the graph for editing. + */ +export async function loadBlueprintForEditing( + blueprintId: string, + allBlocksData: BuildingBlock[], + conditionsData: BuildingBlock[], + conditionEdgeColor: string, +): Promise { + const blueprintInfo = await getBlueprintInfo(blueprintId); + const specDict = blueprintInfo.spec_dict; + return reconstructBlueprintGraph( + specDict, + allBlocksData, + conditionsData, + conditionEdgeColor, + ); +} diff --git a/ui/client/src/pages/AgenticWorkflows.tsx b/ui/client/src/pages/AgenticWorkflows.tsx index 2a366182b..7e2d1e006 100644 --- a/ui/client/src/pages/AgenticWorkflows.tsx +++ b/ui/client/src/pages/AgenticWorkflows.tsx @@ -36,6 +36,7 @@ export default function AgenticWorkflows() { const [builtGraphName, setBuiltGraphName] = useState(null); const [selectedGraphId, setSelectedGraphId] = useState(null); const [showGraphBuilder, setShowGraphBuilder] = useState(false); + const [editingBlueprintId, setEditingBlueprintId] = useState(null); const [isLoadingFlow, setIsLoadingFlow] = useState(false); const [isFlowValid, setIsFlowValid] = useState(true); const [isValidatingFlow, setIsValidatingFlow] = useState(false); @@ -96,16 +97,20 @@ export default function AgenticWorkflows() { }; const handleBuildGraph = () => { + setEditingBlueprintId(null); + setShowGraphBuilder(true); + }; + + const handleEditWorkflow = (flow: FlowObject) => { + setEditingBlueprintId(flow.id); setShowGraphBuilder(true); }; const handleBackToFlowConfig = useCallback((savedBlueprint?: SavedBlueprintInfo) => { setShowGraphBuilder(false); + setEditingBlueprintId(null); - // If a blueprint was just saved, select it in the workflow list - if (savedBlueprint) { - // Create a minimal FlowObject to select the newly saved blueprint - // The WorkflowsPanel will fetch the full data and match by ID + if (savedBlueprint?.blueprintId) { setSelectedFlow({ id: savedBlueprint.blueprintId, name: savedBlueprint.name, @@ -113,6 +118,10 @@ export default function AgenticWorkflows() { icon: null, flow: { nodes: [], edges: [] }, } as FlowObject); + } else { + // Going back without saving (new build or edit) — clear selection so + // WorkflowsPanel remounts cleanly and auto-selects a flow. + setSelectedFlow(null); } }, []); @@ -128,7 +137,10 @@ export default function AgenticWorkflows() {
{showGraphBuilder ? ( - + ) : (
diff --git a/ui/client/src/workspace/NewGraph.tsx b/ui/client/src/workspace/NewGraph.tsx index d966831cb..9366cf574 100644 --- a/ui/client/src/workspace/NewGraph.tsx +++ b/ui/client/src/workspace/NewGraph.tsx @@ -8,9 +8,10 @@ import SaveBlueprintModal from "@/components/agentic-ai/graphs/SaveBlueprintModa interface NewGraphProps { onBack?: (savedBlueprint?: SavedBlueprintInfo) => void; + editBlueprintId?: string | null; } -export default function NewGraph({ onBack }: NewGraphProps) { +export default function NewGraph({ onBack, editBlueprintId }: NewGraphProps) { const [sidebarOpen, setSidebarOpen] = useState(false); const { @@ -40,7 +41,10 @@ export default function NewGraph({ onBack }: NewGraphProps) { fixSuggestions, isValidating, isSaving, - } = useGraphLogic({ onSaveComplete: onBack }); + isEditMode, + editBlueprintName, + editBlueprintDescription, + } = useGraphLogic({ onSaveComplete: onBack, editBlueprintId }); const [saveModalOpen, setSaveModalOpen] = useState(false); @@ -116,6 +120,7 @@ export default function NewGraph({ onBack }: NewGraphProps) { onAttachCondition={attachConditionToNode} onRemoveCondition={removeConditionFromNode} isGraphValid={isGraphValid} + isEditMode={isEditMode} />
@@ -145,6 +150,9 @@ export default function NewGraph({ onBack }: NewGraphProps) { onClose={() => setSaveModalOpen(false)} onSave={saveGraph} isLoading={isSaving} + isEditMode={isEditMode} + defaultName={editBlueprintName} + defaultDescription={editBlueprintDescription} /> ); From d1139a65dbc747482b5e405037d5d276fe5c9e8c Mon Sep 17 00:00:00 2001 From: MayaCrmi Date: Tue, 24 Feb 2026 03:16:10 -0500 Subject: [PATCH 02/10] more thing --- ui/client/src/hooks/use-load-blueprint.ts | 114 ++++++---------------- 1 file changed, 30 insertions(+), 84 deletions(-) diff --git a/ui/client/src/hooks/use-load-blueprint.ts b/ui/client/src/hooks/use-load-blueprint.ts index cf619fd28..9307065be 100644 --- a/ui/client/src/hooks/use-load-blueprint.ts +++ b/ui/client/src/hooks/use-load-blueprint.ts @@ -1,4 +1,5 @@ import { Node, Edge, MarkerType } from "reactflow"; +import dagre from "@dagrejs/dagre"; import { BuildingBlock } from "@/types/graph"; import { getCategoryDisplay } from "@/components/shared/helpers"; import { getBlueprintInfo } from "@/api/blueprints"; @@ -55,114 +56,59 @@ function findBlockByRid(rid: string, blocks: BuildingBlock[]): BuildingBlock | n ); } +const NODE_WIDTH = 320; +const NODE_HEIGHT = 80; + /** - * Compute a hierarchical layout for plan steps using topological levels, - * matching the display approach used by the workflow list (JointJS layout). + * Compute a hierarchical layout for plan steps using dagre, + * matching the display produced by JointJS DirectedGraph.layout. */ function computeLayout( plan: YamlFlowPlanStep[], - specNodes: YamlFlowNode[], + _specNodes: YamlFlowNode[], ): Map { - const positions = new Map(); + const g = new dagre.graphlib.Graph(); + g.setGraph({ + rankdir: "TB", + nodesep: 60, + edgesep: 40, + ranksep: 80, + marginx: 32, + marginy: 32, + }); + g.setDefaultEdgeLabel(() => ({})); - // Build node-type lookup from spec nodes - const nodeDefByRef = new Map(); - for (const n of specNodes) { - nodeDefByRef.set(stripRef(n.rid), n); - } - const typeByUid = new Map(); for (const step of plan) { - const def = nodeDefByRef.get(step.node); - typeByUid.set(step.uid, def?.type || "custom_agent_node"); + g.setNode(step.uid, { width: NODE_WIDTH, height: NODE_HEIGHT }); } - // Build predecessor map from `after` + `branches` - const predecessors: Record = {}; + const edgeSet = new Set(); for (const step of plan) { - if (!predecessors[step.uid]) predecessors[step.uid] = []; if (step.after) { const afters = Array.isArray(step.after) ? step.after : [step.after]; - predecessors[step.uid].push(...afters); + for (const a of afters) { + const key = `${a}->${step.uid}`; + if (!edgeSet.has(key)) { g.setEdge(a, step.uid); edgeSet.add(key); } + } } if (step.branches) { for (const targetUid of Object.values(step.branches)) { const tid = targetUid as string; - if (!predecessors[tid]) predecessors[tid] = []; - if (!predecessors[tid].includes(step.uid)) { - predecessors[tid].push(step.uid); - } + const key = `${step.uid}->${tid}`; + if (!edgeSet.has(key)) { g.setEdge(step.uid, tid); edgeSet.add(key); } } } } - // Assign levels via iterative BFS - const level: Record = {}; + dagre.layout(g); - // Level 0: user_question_node - for (const step of plan) { - if (typeByUid.get(step.uid) === "user_question_node") { - level[step.uid] = 0; - } - } - - let changed = true; - let iterations = 0; - const maxIter = plan.length * plan.length + 1; - while (changed && iterations < maxIter) { - changed = false; - iterations++; - for (const step of plan) { - if (level[step.uid] !== undefined) continue; - if (typeByUid.get(step.uid) === "final_answer_node") continue; - const preds = predecessors[step.uid] || []; - if (preds.length === 0) { - level[step.uid] = 1; - changed = true; - } else if (preds.every((p) => level[p] !== undefined)) { - level[step.uid] = Math.max(...preds.map((p) => level[p])) + 1; - changed = true; - } - } - } - - // Fallback for any unresolved non-final nodes - const maxLvl = Math.max(0, ...Object.values(level).filter((v) => v !== undefined)); - for (const step of plan) { - if (level[step.uid] === undefined && typeByUid.get(step.uid) !== "final_answer_node") { - level[step.uid] = maxLvl + 1; - } - } - - // Final answer always last - const finalMaxLvl = Math.max(0, ...Object.values(level).filter((v) => v !== undefined)); + const positions = new Map(); for (const step of plan) { - if (typeByUid.get(step.uid) === "final_answer_node") { - level[step.uid] = finalMaxLvl + 1; + const n = g.node(step.uid); + if (n) { + positions.set(step.uid, { x: n.x - NODE_WIDTH / 2, y: n.y - NODE_HEIGHT / 2 }); } } - - // Group by level - const nodesByLevel: Record = {}; - for (const step of plan) { - const l = level[step.uid] ?? 0; - if (!nodesByLevel[l]) nodesByLevel[l] = []; - nodesByLevel[l].push(step.uid); - } - - // Position nodes — spread siblings horizontally, levels vertically - const Y_SPACING = 200; - const X_SPACING = 300; - const X_CENTER = 400; - - for (const [lvl, uids] of Object.entries(nodesByLevel)) { - const l = Number(lvl); - const totalWidth = (uids.length - 1) * X_SPACING; - const startX = X_CENTER - totalWidth / 2; - uids.forEach((uid, index) => { - positions.set(uid, { x: startX + index * X_SPACING, y: 100 + l * Y_SPACING }); - }); - } - return positions; } From f494e2aa7bfeb2bd085e203bea2b27005174d487 Mon Sep 17 00:00:00 2001 From: MayaCrmi Date: Wed, 25 Feb 2026 02:54:06 -0500 Subject: [PATCH 03/10] before CRing --- .../agentic-ai/graphs/SaveBlueprintModal.tsx | 24 +++++++++---------- ui/client/src/hooks/use-graph-logic.ts | 2 ++ ui/client/src/pages/AgenticWorkflows.tsx | 13 ++++------ ui/client/src/workspace/NewGraph.tsx | 4 ++-- 4 files changed, 20 insertions(+), 23 deletions(-) diff --git a/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx b/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx index 91d78cc8b..f596fe1fa 100644 --- a/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx +++ b/ui/client/src/components/agentic-ai/graphs/SaveBlueprintModal.tsx @@ -20,8 +20,8 @@ interface SaveBlueprintModalProps { onSave: (name: string, description: string) => void; isLoading?: boolean; isEditMode?: boolean; - defaultName?: string; - defaultDescription?: string; + currentName?: string; + currentDescription?: string; } const SaveBlueprintModal: React.FC = ({ @@ -30,18 +30,18 @@ const SaveBlueprintModal: React.FC = ({ onSave, isLoading = false, isEditMode = false, - defaultName = "", - defaultDescription = "", + currentName = "", + currentDescription = "", }) => { - const [name, setName] = useState(defaultName); - const [description, setDescription] = useState(defaultDescription); + const [name, setName] = useState(currentName); + const [description, setDescription] = useState(currentDescription); useEffect(() => { if (isEditMode) { - setName(defaultName); - setDescription(defaultDescription); + setName(currentName); + setDescription(currentDescription); } - }, [isEditMode, defaultName, defaultDescription]); + }, [isEditMode, currentName, currentDescription]); const handleSave = () => { if (!name.trim()) { @@ -51,8 +51,8 @@ const SaveBlueprintModal: React.FC = ({ }; const handleClose = () => { - setName(isEditMode ? defaultName : ""); - setDescription(isEditMode ? defaultDescription : ""); + setName(isEditMode ? currentName : ""); + setDescription(isEditMode ? currentDescription : ""); onClose(); }; @@ -74,7 +74,7 @@ const SaveBlueprintModal: React.FC = ({ setName(e.target.value)} className="input-dark-theme bg-input border-border text-foreground" diff --git a/ui/client/src/hooks/use-graph-logic.ts b/ui/client/src/hooks/use-graph-logic.ts index 284e30e49..c808a68ba 100644 --- a/ui/client/src/hooks/use-graph-logic.ts +++ b/ui/client/src/hooks/use-graph-logic.ts @@ -1050,6 +1050,7 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { try { setIsSaving(true); + // Update yamlFlow with name and description const updatedYamlFlow = { ...yamlFlow, name: name, @@ -1058,6 +1059,7 @@ export const useGraphLogic = (options: UseGraphLogicOptions = {}) => { setYamlFlow(updatedYamlFlow); + // Convert to YAML string using js-yaml library const yamlString = yaml.dump(updatedYamlFlow, { indent: 2, lineWidth: -1, diff --git a/ui/client/src/pages/AgenticWorkflows.tsx b/ui/client/src/pages/AgenticWorkflows.tsx index 7e2d1e006..ca80bc65c 100644 --- a/ui/client/src/pages/AgenticWorkflows.tsx +++ b/ui/client/src/pages/AgenticWorkflows.tsx @@ -96,13 +96,8 @@ export default function AgenticWorkflows() { } }; - const handleBuildGraph = () => { - setEditingBlueprintId(null); - setShowGraphBuilder(true); - }; - - const handleEditWorkflow = (flow: FlowObject) => { - setEditingBlueprintId(flow.id); + const handleOpenGraphBuilder = (flow?: FlowObject) => { + setEditingBlueprintId(flow?.id ?? null); setShowGraphBuilder(true); }; @@ -197,7 +192,7 @@ export default function AgenticWorkflows() {