matrixcode_tui/workflow/
mermaid.rs1use matrixcode_core::workflow::{WorkflowDef, WorkflowContext, NodeType};
6
7pub fn export_mermaid(def: &WorkflowDef, ctx: Option<&WorkflowContext>) -> String {
9 let mut output = String::new();
10
11 output.push_str("```mermaid\n");
13 output.push_str("flowchart TD\n");
14
15 for node in &def.nodes {
17 let label = &node.name;
18 let icon = node_type_icon_mermaid(&node.node_type);
19
20 let status_class = if let Some(context) = ctx {
22 if let Some(exec) = context.node_executions.get(&node.id) {
23 match exec.status {
24 matrixcode_core::workflow::NodeStatus::Pending => "pending",
25 matrixcode_core::workflow::NodeStatus::Running => "running",
26 matrixcode_core::workflow::NodeStatus::Completed => "completed",
27 matrixcode_core::workflow::NodeStatus::Failed => "failed",
28 matrixcode_core::workflow::NodeStatus::Skipped => "skipped",
29 }
30 } else {
31 "pending"
32 }
33 } else {
34 ""
35 };
36
37 let safe_id = sanitize_id(&node.id);
39
40 output.push_str(&format!(" {}[\"{} {}\"]\n", safe_id, icon, label));
42
43 if !status_class.is_empty() {
45 output.push_str(&format!(" class {} {}\n", safe_id, status_class));
46 }
47 }
48
49 for edge in &def.edges {
51 let from_safe = sanitize_id(&edge.from);
52 let to_safe = sanitize_id(&edge.to);
53
54 if let Some(ref condition) = edge.condition {
55 output.push_str(&format!(" {} -- {} --> {}\n", from_safe, condition, to_safe));
56 } else {
57 output.push_str(&format!(" {} --> {}\n", from_safe, to_safe));
58 }
59 }
60
61 output.push('\n');
63 output.push_str(" classDef pending fill:#f9f9f9,stroke:#999,stroke-width:1px\n");
64 output.push_str(" classDef running fill:#fff3cd,stroke:#ffc107,stroke-width:3px\n");
65 output.push_str(" classDef completed fill:#d4edda,stroke:#28a745,stroke-width:2px\n");
66 output.push_str(" classDef failed fill:#f8d7da,stroke:#dc3545,stroke-width:2px\n");
67 output.push_str(" classDef skipped fill:#e2e3e5,stroke:#6c757d,stroke-width:1px\n");
68
69 output.push_str("```\n");
71
72 output
73}
74
75pub fn export_mermaid_with_summary(def: &WorkflowDef, ctx: &WorkflowContext) -> String {
77 let mut output = export_mermaid(def, Some(ctx));
78
79 output.push_str("\n## Execution Summary\n\n");
81
82 let status = match ctx.status {
83 matrixcode_core::workflow::WorkflowStatus::Pending => "Pending",
84 matrixcode_core::workflow::WorkflowStatus::Running => "Running",
85 matrixcode_core::workflow::WorkflowStatus::Completed => "β
Completed",
86 matrixcode_core::workflow::WorkflowStatus::Failed => "β Failed",
87 matrixcode_core::workflow::WorkflowStatus::Paused => "βΈοΈ Paused",
88 matrixcode_core::workflow::WorkflowStatus::Cancelled => "π« Cancelled",
89 };
90
91 output.push_str(&format!("**Status**: {}\n\n", status));
92 output.push_str(&format!("**Instance ID**: {}\n\n", ctx.instance_id));
93 output.push_str(&format!("**Nodes executed**: {}\n\n", ctx.execution_path.len()));
94
95 if let Some(ref error) = ctx.error {
96 output.push_str(&format!("**Error**: {}\n\n", error));
97 }
98
99 output.push_str("### Node Details\n\n");
101 output.push_str("| Node | Status | Duration |\n");
102 output.push_str("|------|--------|----------|\n");
103
104 for node in &def.nodes {
105 let exec = ctx.node_executions.get(&node.id);
106 let (status_icon, duration) = if let Some(e) = exec {
107 let icon = match e.status {
108 matrixcode_core::workflow::NodeStatus::Pending => "β",
109 matrixcode_core::workflow::NodeStatus::Running => "β³",
110 matrixcode_core::workflow::NodeStatus::Completed => "β",
111 matrixcode_core::workflow::NodeStatus::Failed => "β",
112 matrixcode_core::workflow::NodeStatus::Skipped => "β",
113 };
114 let duration = if let (Some(start), Some(end)) = (e.started_at, e.finished_at) {
115 let ms = (end - start).num_milliseconds();
116 format!("{}ms", ms)
117 } else {
118 "-".to_string()
119 };
120 (icon, duration)
121 } else {
122 ("β", "-".to_string())
123 };
124
125 output.push_str(&format!("| {} | {} {} | {} |\n", node.name, status_icon, node.id, duration));
126 }
127
128 output
129}
130
131fn node_type_icon_mermaid(node_type: &NodeType) -> &'static str {
133 match node_type {
134 NodeType::Start => "βΆ",
135 NodeType::End => "β ",
136 NodeType::Task => "β",
137 NodeType::Condition => "β",
138 NodeType::Parallel => "β",
139 NodeType::Approval => "?",
140 NodeType::Wait => "β³",
141 NodeType::SubWorkflow => "β³",
142 }
143}
144
145fn sanitize_id(id: &str) -> String {
148 const RESERVED: &[&str] = &["end", "start", "subgraph", "direction", "style", "class", "linkstyle"];
150
151 let sanitized: String = id.chars()
153 .map(|c| {
154 if c.is_alphanumeric() || c == '_' {
155 c
156 } else {
157 '_'
158 }
159 })
160 .collect();
161
162 if RESERVED.contains(&sanitized.as_str()) {
164 format!("n_{}", sanitized)
165 } else {
166 sanitized
167 }
168}
169
170pub fn export_workflow_mermaid(
172 def: &WorkflowDef,
173 ctx: Option<&WorkflowContext>,
174 output_path: &std::path::Path,
175) -> std::io::Result<()> {
176 let content = if let Some(context) = ctx {
177 export_mermaid_with_summary(def, context)
178 } else {
179 export_mermaid(def, None)
180 };
181
182 std::fs::write(output_path, content)
183}