Spaces:
Running
Running
feat: Upgrade AutoLayoutEngine to support rich node generation (color, status, progress, tags, subgoals, etc.)
b01b49e | using System; | |
| using System.Collections.Generic; | |
| using System.Linq; | |
| using FlowAPI.Application.DTOs.AI; | |
| using FlowAPI.Application.DTOs.Graph; | |
| namespace FlowAPI.Application.Services | |
| { | |
| public static class AutoLayoutEngine | |
| { | |
| private const double StepX = 300.0; | |
| private const double StepY = 180.0; | |
| public static FullGraphResponseDto LayoutGraph(QuestSchemaDto schema, Guid userId) | |
| { | |
| var graphId = Guid.NewGuid(); | |
| var response = new FullGraphResponseDto | |
| { | |
| Id = graphId, | |
| UserId = userId, | |
| Title = schema.Title, | |
| Description = schema.Description, | |
| CreatedAt = DateTime.UtcNow, | |
| IsShared = false, | |
| Permission = "Owner", | |
| Nodes = new List<NodeDto>(), | |
| Edges = new List<EdgeDto>() | |
| }; | |
| if (schema.Nodes == null || !schema.Nodes.Any()) | |
| return response; | |
| // 1. Map string string-based IDs from LLM into Guids | |
| var idMap = new Dictionary<string, Guid>(); | |
| foreach (var node in schema.Nodes) | |
| { | |
| idMap[node.Id] = Guid.NewGuid(); | |
| } | |
| // 2. Calculate logical layers (Sugiyama-like ranking) | |
| var layerMap = new Dictionary<string, int>(); | |
| foreach (var node in schema.Nodes) | |
| { | |
| layerMap[node.Id] = 0; // Default layer | |
| } | |
| // Transitively propagate layer ranks based on dependencies (max N passes) | |
| int n = schema.Nodes.Count; | |
| for (int pass = 0; pass < n; pass++) | |
| { | |
| bool changed = false; | |
| foreach (var node in schema.Nodes) | |
| { | |
| if (!string.IsNullOrEmpty(node.DependsOn) && idMap.ContainsKey(node.DependsOn)) | |
| { | |
| int parentLayer = layerMap[node.DependsOn]; | |
| int currentLayer = layerMap[node.Id]; | |
| if (currentLayer < parentLayer + 1) | |
| { | |
| layerMap[node.Id] = parentLayer + 1; | |
| changed = true; | |
| } | |
| } | |
| } | |
| if (!changed) break; // Optimization if stabilized early | |
| } | |
| // 3. Group nodes by layers and calculate coordinates | |
| var layers = new Dictionary<int, List<QuestNodeDto>>(); | |
| foreach (var node in schema.Nodes) | |
| { | |
| int layer = layerMap[node.Id]; | |
| if (!layers.ContainsKey(layer)) | |
| { | |
| layers[layer] = new List<QuestNodeDto>(); | |
| } | |
| layers[layer].Add(node); | |
| } | |
| // Set coordinates symmetrically per layer | |
| foreach (var layerKvp in layers) | |
| { | |
| int layer = layerKvp.Key; | |
| var layerNodes = layerKvp.Value; | |
| int count = layerNodes.Count; | |
| for (int i = 0; i < count; i++) | |
| { | |
| var node = layerNodes[i]; | |
| var nodeGuid = idMap[node.Id]; | |
| // Symmetric distribution horizontally around X = 0, and sequential progression top-to-bottom | |
| double posX = (i - (count - 1) / 2.0) * StepX; | |
| double posY = layer * StepY; | |
| // Determine visual properties | |
| string? nodeColor = node.Color; | |
| if (string.IsNullOrEmpty(nodeColor) && !string.IsNullOrEmpty(node.Condition)) | |
| { | |
| if (node.Condition.Equals("Fail", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| nodeColor = "pink"; | |
| } | |
| else if (node.Condition.Equals("Success", StringComparison.OrdinalIgnoreCase)) | |
| { | |
| nodeColor = "green"; | |
| } | |
| } | |
| // Map Status string to integer State Enum value (Todo = 0, In Progress = 1, Review = 2, Done = 3) | |
| int stateVal = 0; | |
| if (!string.IsNullOrEmpty(node.Status)) | |
| { | |
| if (node.Status.Equals("Done", StringComparison.OrdinalIgnoreCase)) stateVal = 3; | |
| else if (node.Status.Equals("Review", StringComparison.OrdinalIgnoreCase)) stateVal = 2; | |
| else if (node.Status.Equals("In Progress", StringComparison.OrdinalIgnoreCase) || node.Status.Equals("Progress", StringComparison.OrdinalIgnoreCase)) stateVal = 1; | |
| } | |
| // Build rich data object for serialization into Decision column | |
| var decisionObj = new Dictionary<string, object?>(); | |
| decisionObj["decision"] = node.Condition; | |
| decisionObj["description"] = node.Description ?? ""; | |
| decisionObj["progress"] = node.Progress ?? 0; | |
| decisionObj["showProgress"] = node.ShowProgress ?? false; | |
| decisionObj["color"] = nodeColor ?? "default"; | |
| decisionObj["isPinned"] = node.IsPinned ?? false; | |
| decisionObj["tags"] = node.Tags ?? new List<string>(); | |
| decisionObj["status"] = node.Status ?? "Todo"; | |
| decisionObj["assignees"] = node.Assignees ?? new List<QuestAssigneeDto>(); | |
| if (node.Subgoals != null) | |
| { | |
| decisionObj["subgoals"] = new { total = node.Subgoals.Total, done = node.Subgoals.Done }; | |
| } | |
| if (node.Options != null) | |
| { | |
| decisionObj["options"] = node.Options.Select(o => new { id = o.Id, label = o.Label }).ToList(); | |
| } | |
| if (node.SelectedOptions != null) | |
| { | |
| decisionObj["selectedOptions"] = node.SelectedOptions; | |
| } | |
| if (node.MultiSelect != null) | |
| { | |
| decisionObj["multiSelect"] = node.MultiSelect.Value; | |
| } | |
| if (node.ResetRule != null) | |
| { | |
| decisionObj["resetRule"] = node.ResetRule; | |
| } | |
| if (!string.IsNullOrEmpty(node.Url)) | |
| { | |
| decisionObj["url"] = node.Url; | |
| } | |
| // For image/path/text nodes, save payload inside Data if needed | |
| string? nodeData = null; | |
| if (node.Type == "image" && !string.IsNullOrEmpty(node.Url)) | |
| { | |
| nodeData = node.Url; | |
| } | |
| string decisionJson = System.Text.Json.JsonSerializer.Serialize(decisionObj); | |
| response.Nodes.Add(new NodeDto | |
| { | |
| Id = nodeGuid, | |
| Label = node.Label, | |
| PosX = posX, | |
| PosY = posY, | |
| Type = node.Type ?? "sketch", | |
| State = stateVal, | |
| Decision = decisionJson, // JSON string matches frontend expected parsing! | |
| Description = node.Description, | |
| Color = nodeColor, | |
| IsPinned = node.IsPinned ?? false, | |
| Width = node.Width ?? 220, | |
| Height = node.Height ?? 116, | |
| Data = nodeData | |
| }); | |
| // 4. Create Edges | |
| if (!string.IsNullOrEmpty(node.DependsOn) && idMap.ContainsKey(node.DependsOn)) | |
| { | |
| response.Edges.Add(new EdgeDto | |
| { | |
| Id = Guid.NewGuid(), | |
| FromNodeId = idMap[node.DependsOn], | |
| ToNodeId = nodeGuid, | |
| Condition = node.Condition ?? "True" | |
| }); | |
| } | |
| } | |
| } | |
| return response; | |
| } | |
| } | |
| } | |