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::{WorkflowDef, WorkflowContext, NodeType};
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!("    {} -- {} --> {}\n", from_safe, condition, to_safe));
56        } else {
57            output.push_str(&format!("    {} --> {}\n", from_safe, to_safe));
58        }
59    }
60
61    // Style definitions
62    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    // Footer
70    output.push_str("```\n");
71
72    output
73}
74
75/// Export workflow to Mermaid with execution summary
76pub fn export_mermaid_with_summary(def: &WorkflowDef, ctx: &WorkflowContext) -> String {
77    let mut output = export_mermaid(def, Some(ctx));
78
79    // Add execution summary
80    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    // Node execution details
100    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
131/// Get node type icon for mermaid
132fn 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
145/// Sanitize node ID for mermaid syntax
146/// Mermaid has reserved keywords like 'end', 'start', 'subgraph' that cannot be used as node IDs
147fn sanitize_id(id: &str) -> String {
148    // Mermaid reserved keywords that cause parse errors
149    const RESERVED: &[&str] = &["end", "start", "subgraph", "direction", "style", "class", "linkstyle"];
150
151    // First sanitize characters
152    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    // Prefix reserved keywords with 'n_' to avoid conflicts
163    if RESERVED.contains(&sanitized.as_str()) {
164        format!("n_{}", sanitized)
165    } else {
166        sanitized
167    }
168}
169
170/// Export workflow instance to file
171pub 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}