Skip to main content

matrixcode_tui/workflow/
mermaid.rs

1//! Mermaid Export
2//!
3//! Export workflow DAG to Mermaid diagram syntax
4
5use matrixcode_core::workflow::{NodeType, WorkflowContext, WorkflowDef};
6
7/// Export workflow to Mermaid flowchart syntax
8pub fn export_mermaid(def: &WorkflowDef, ctx: Option<&WorkflowContext>) -> String {
9    let mut output = String::new();
10
11    // Header
12    output.push_str("```mermaid\n");
13    output.push_str("flowchart TD\n");
14
15    // Node definitions with labels
16    for node in &def.nodes {
17        let label = &node.name;
18        let icon = node_type_icon_mermaid(&node.node_type);
19
20        // Determine status for styling
21        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        // Sanitize node ID for mermaid (replace special chars)
38        let safe_id = sanitize_id(&node.id);
39
40        // Node definition
41        output.push_str(&format!("    {}[\"{} {}\"]\n", safe_id, icon, label));
42
43        // Apply class for styling
44        if !status_class.is_empty() {
45            output.push_str(&format!("    class {} {}\n", safe_id, status_class));
46        }
47    }
48
49    // Edge definitions
50    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    // Style definitions
65    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    // Footer
73    output.push_str("```\n");
74
75    output
76}
77
78/// Export workflow to Mermaid with execution summary
79pub fn export_mermaid_with_summary(def: &WorkflowDef, ctx: &WorkflowContext) -> String {
80    let mut output = export_mermaid(def, Some(ctx));
81
82    // Add execution summary
83    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    // Node execution details
106    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
140/// Get node type icon for mermaid
141fn 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
154/// Sanitize node ID for mermaid syntax
155/// Mermaid has reserved keywords like 'end', 'start', 'subgraph' that cannot be used as node IDs
156fn sanitize_id(id: &str) -> String {
157    // Mermaid reserved keywords that cause parse errors
158    const RESERVED: &[&str] = &[
159        "end",
160        "start",
161        "subgraph",
162        "direction",
163        "style",
164        "class",
165        "linkstyle",
166    ];
167
168    // First sanitize characters
169    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    // Prefix reserved keywords with 'n_' to avoid conflicts
181    if RESERVED.contains(&sanitized.as_str()) {
182        format!("n_{}", sanitized)
183    } else {
184        sanitized
185    }
186}
187
188/// Export workflow instance to file
189pub 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}