matrixcode_tui/workflow/
mermaid.rs1use matrixcode_core::workflow::{NodeType, WorkflowContext, WorkflowDef};
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!(
56 " {} -- {} --> {}\n",
57 from_safe, condition, to_safe
58 ));
59 } else {
60 output.push_str(&format!(" {} --> {}\n", from_safe, to_safe));
61 }
62 }
63
64 output.push('\n');
66 output.push_str(" classDef pending fill:#f9f9f9,stroke:#999,stroke-width:1px\n");
67 output.push_str(" classDef running fill:#fff3cd,stroke:#ffc107,stroke-width:3px\n");
68 output.push_str(" classDef completed fill:#d4edda,stroke:#28a745,stroke-width:2px\n");
69 output.push_str(" classDef failed fill:#f8d7da,stroke:#dc3545,stroke-width:2px\n");
70 output.push_str(" classDef skipped fill:#e2e3e5,stroke:#6c757d,stroke-width:1px\n");
71
72 output.push_str("```\n");
74
75 output
76}
77
78pub fn export_mermaid_with_summary(def: &WorkflowDef, ctx: &WorkflowContext) -> String {
80 let mut output = export_mermaid(def, Some(ctx));
81
82 output.push_str("\n## Execution Summary\n\n");
84
85 let status = match ctx.status {
86 matrixcode_core::workflow::WorkflowStatus::Pending => "Pending",
87 matrixcode_core::workflow::WorkflowStatus::Running => "Running",
88 matrixcode_core::workflow::WorkflowStatus::Completed => "β
Completed",
89 matrixcode_core::workflow::WorkflowStatus::Failed => "β Failed",
90 matrixcode_core::workflow::WorkflowStatus::Paused => "βΈοΈ Paused",
91 matrixcode_core::workflow::WorkflowStatus::Cancelled => "π« Cancelled",
92 };
93
94 output.push_str(&format!("**Status**: {}\n\n", status));
95 output.push_str(&format!("**Instance ID**: {}\n\n", ctx.instance_id));
96 output.push_str(&format!(
97 "**Nodes executed**: {}\n\n",
98 ctx.execution_path.len()
99 ));
100
101 if let Some(ref error) = ctx.error {
102 output.push_str(&format!("**Error**: {}\n\n", error));
103 }
104
105 output.push_str("### Node Details\n\n");
107 output.push_str("| Node | Status | Duration |\n");
108 output.push_str("|------|--------|----------|\n");
109
110 for node in &def.nodes {
111 let exec = ctx.node_executions.get(&node.id);
112 let (status_icon, duration) = if let Some(e) = exec {
113 let icon = match e.status {
114 matrixcode_core::workflow::NodeStatus::Pending => "β",
115 matrixcode_core::workflow::NodeStatus::Running => "β³",
116 matrixcode_core::workflow::NodeStatus::Completed => "β",
117 matrixcode_core::workflow::NodeStatus::Failed => "β",
118 matrixcode_core::workflow::NodeStatus::Skipped => "β",
119 };
120 let duration = if let (Some(start), Some(end)) = (e.started_at, e.finished_at) {
121 let ms = (end - start).num_milliseconds();
122 format!("{}ms", ms)
123 } else {
124 "-".to_string()
125 };
126 (icon, duration)
127 } else {
128 ("β", "-".to_string())
129 };
130
131 output.push_str(&format!(
132 "| {} | {} {} | {} |\n",
133 node.name, status_icon, node.id, duration
134 ));
135 }
136
137 output
138}
139
140fn node_type_icon_mermaid(node_type: &NodeType) -> &'static str {
142 match node_type {
143 NodeType::Start => "βΆ",
144 NodeType::End => "β ",
145 NodeType::Task => "β",
146 NodeType::Condition => "β",
147 NodeType::Parallel => "β",
148 NodeType::Approval => "?",
149 NodeType::Wait => "β³",
150 NodeType::SubWorkflow => "β³",
151 }
152}
153
154fn sanitize_id(id: &str) -> String {
157 const RESERVED: &[&str] = &[
159 "end",
160 "start",
161 "subgraph",
162 "direction",
163 "style",
164 "class",
165 "linkstyle",
166 ];
167
168 let sanitized: String = id
170 .chars()
171 .map(|c| {
172 if c.is_alphanumeric() || c == '_' {
173 c
174 } else {
175 '_'
176 }
177 })
178 .collect();
179
180 if RESERVED.contains(&sanitized.as_str()) {
182 format!("n_{}", sanitized)
183 } else {
184 sanitized
185 }
186}
187
188pub fn export_workflow_mermaid(
190 def: &WorkflowDef,
191 ctx: Option<&WorkflowContext>,
192 output_path: &std::path::Path,
193) -> std::io::Result<()> {
194 let content = if let Some(context) = ctx {
195 export_mermaid_with_summary(def, context)
196 } else {
197 export_mermaid(def, None)
198 };
199
200 std::fs::write(output_path, content)
201}