use matrixcode_core::workflow::{NodeType, WorkflowContext, WorkflowDef};
pub fn export_mermaid(def: &WorkflowDef, ctx: Option<&WorkflowContext>) -> String {
let mut output = String::new();
output.push_str("```mermaid\n");
output.push_str("flowchart TD\n");
for node in &def.nodes {
let label = &node.name;
let icon = node_type_icon_mermaid(&node.node_type);
let status_class = if let Some(context) = ctx {
if let Some(exec) = context.node_executions.get(&node.id) {
match exec.status {
matrixcode_core::workflow::NodeStatus::Pending => "pending",
matrixcode_core::workflow::NodeStatus::Running => "running",
matrixcode_core::workflow::NodeStatus::Completed => "completed",
matrixcode_core::workflow::NodeStatus::Failed => "failed",
matrixcode_core::workflow::NodeStatus::Skipped => "skipped",
}
} else {
"pending"
}
} else {
""
};
let safe_id = sanitize_id(&node.id);
output.push_str(&format!(" {}[\"{} {}\"]\n", safe_id, icon, label));
if !status_class.is_empty() {
output.push_str(&format!(" class {} {}\n", safe_id, status_class));
}
}
for edge in &def.edges {
let from_safe = sanitize_id(&edge.from);
let to_safe = sanitize_id(&edge.to);
if let Some(ref condition) = edge.condition {
output.push_str(&format!(
" {} -- {} --> {}\n",
from_safe, condition, to_safe
));
} else {
output.push_str(&format!(" {} --> {}\n", from_safe, to_safe));
}
}
output.push('\n');
output.push_str(" classDef pending fill:#f9f9f9,stroke:#999,stroke-width:1px\n");
output.push_str(" classDef running fill:#fff3cd,stroke:#ffc107,stroke-width:3px\n");
output.push_str(" classDef completed fill:#d4edda,stroke:#28a745,stroke-width:2px\n");
output.push_str(" classDef failed fill:#f8d7da,stroke:#dc3545,stroke-width:2px\n");
output.push_str(" classDef skipped fill:#e2e3e5,stroke:#6c757d,stroke-width:1px\n");
output.push_str("```\n");
output
}
pub fn export_mermaid_with_summary(def: &WorkflowDef, ctx: &WorkflowContext) -> String {
let mut output = export_mermaid(def, Some(ctx));
output.push_str("\n## Execution Summary\n\n");
let status = match ctx.status {
matrixcode_core::workflow::WorkflowStatus::Pending => "Pending",
matrixcode_core::workflow::WorkflowStatus::Running => "Running",
matrixcode_core::workflow::WorkflowStatus::Completed => "β
Completed",
matrixcode_core::workflow::WorkflowStatus::Failed => "β Failed",
matrixcode_core::workflow::WorkflowStatus::Paused => "βΈοΈ Paused",
matrixcode_core::workflow::WorkflowStatus::Cancelled => "π« Cancelled",
};
output.push_str(&format!("**Status**: {}\n\n", status));
output.push_str(&format!("**Instance ID**: {}\n\n", ctx.instance_id));
output.push_str(&format!(
"**Nodes executed**: {}\n\n",
ctx.execution_path.len()
));
if let Some(ref error) = ctx.error {
output.push_str(&format!("**Error**: {}\n\n", error));
}
output.push_str("### Node Details\n\n");
output.push_str("| Node | Status | Duration |\n");
output.push_str("|------|--------|----------|\n");
for node in &def.nodes {
let exec = ctx.node_executions.get(&node.id);
let (status_icon, duration) = if let Some(e) = exec {
let icon = match e.status {
matrixcode_core::workflow::NodeStatus::Pending => "β",
matrixcode_core::workflow::NodeStatus::Running => "β³",
matrixcode_core::workflow::NodeStatus::Completed => "β",
matrixcode_core::workflow::NodeStatus::Failed => "β",
matrixcode_core::workflow::NodeStatus::Skipped => "β",
};
let duration = if let (Some(start), Some(end)) = (e.started_at, e.finished_at) {
let ms = (end - start).num_milliseconds();
format!("{}ms", ms)
} else {
"-".to_string()
};
(icon, duration)
} else {
("β", "-".to_string())
};
output.push_str(&format!(
"| {} | {} {} | {} |\n",
node.name, status_icon, node.id, duration
));
}
output
}
fn node_type_icon_mermaid(node_type: &NodeType) -> &'static str {
match node_type {
NodeType::Start => "βΆ",
NodeType::End => "β ",
NodeType::Task => "β",
NodeType::Condition => "β",
NodeType::Parallel => "β",
NodeType::Approval => "?",
NodeType::Wait => "β³",
NodeType::SubWorkflow => "β³",
}
}
fn sanitize_id(id: &str) -> String {
const RESERVED: &[&str] = &[
"end",
"start",
"subgraph",
"direction",
"style",
"class",
"linkstyle",
];
let sanitized: String = id
.chars()
.map(|c| {
if c.is_alphanumeric() || c == '_' {
c
} else {
'_'
}
})
.collect();
if RESERVED.contains(&sanitized.as_str()) {
format!("n_{}", sanitized)
} else {
sanitized
}
}
pub fn export_workflow_mermaid(
def: &WorkflowDef,
ctx: Option<&WorkflowContext>,
output_path: &std::path::Path,
) -> std::io::Result<()> {
let content = if let Some(context) = ctx {
export_mermaid_with_summary(def, context)
} else {
export_mermaid(def, None)
};
std::fs::write(output_path, content)
}