1use matrixcode_core::workflow::{
6 NodeStatus, NodeType, WorkflowContext, WorkflowDef, WorkflowPersistence, WorkflowRegistry,
7};
8use std::collections::HashMap;
9use std::path::PathBuf;
10
11pub struct WorkflowViewState {
13 pub workflow_def: Option<WorkflowDef>,
15 pub context: Option<WorkflowContext>,
17 pub layout: DagLayout,
19 pub visible: bool,
21 pub selected_node: Option<String>,
23 pub view_mode: WorkflowViewMode,
25 pub spinner_frame: usize,
27}
28
29impl Default for WorkflowViewState {
30 fn default() -> Self {
31 Self {
32 workflow_def: None,
33 context: None,
34 layout: DagLayout::default(),
35 visible: false,
36 selected_node: None,
37 view_mode: WorkflowViewMode::Dag,
38 spinner_frame: 0,
39 }
40 }
41}
42
43#[derive(Default)]
45pub struct DagLayout {
46 pub node_positions: HashMap<String, (usize, usize)>,
48 pub edges: Vec<EdgeInfo>,
50 pub layers: Vec<Vec<String>>,
52 pub height: usize,
54 pub width: usize,
55}
56
57pub struct EdgeInfo {
59 pub from: String,
60 pub to: String,
61 pub condition: Option<String>,
62}
63
64pub enum WorkflowViewMode {
66 Dag,
68 Progress,
70 Detail,
72}
73
74pub struct VisualNode {
76 pub id: String,
77 pub name: String,
78 pub node_type: NodeType,
79 pub status: NodeVisualStatus,
80 pub position: (usize, usize),
81 pub elapsed_ms: Option<u64>,
82}
83
84pub enum NodeVisualStatus {
86 Pending,
87 Running,
88 Completed,
89 Failed { error: String },
90 Skipped,
91}
92
93impl NodeVisualStatus {
94 pub fn icon(&self) -> &'static str {
96 match self {
97 Self::Pending => "○",
98 Self::Running => "⟳",
99 Self::Completed => "✓",
100 Self::Failed { .. } => "✗",
101 Self::Skipped => "→",
102 }
103 }
104
105 pub fn color(&self) -> &'static str {
107 match self {
108 Self::Pending => "gray",
109 Self::Running => "yellow",
110 Self::Completed => "green",
111 Self::Failed { .. } => "red",
112 Self::Skipped => "blue",
113 }
114 }
115}
116
117pub fn node_type_icon(node_type: &NodeType) -> &'static str {
119 match node_type {
120 NodeType::Start => "▶",
121 NodeType::End => "■",
122 NodeType::Task => "⚙",
123 NodeType::Condition => "◇",
124 NodeType::Parallel => "║",
125 NodeType::Approval => "?",
126 NodeType::Wait => "⏳",
127 NodeType::SubWorkflow => "↳",
128 }
129}
130
131pub fn to_visual_status(status: &NodeStatus, error: Option<&String>) -> NodeVisualStatus {
133 match status {
134 NodeStatus::Pending => NodeVisualStatus::Pending,
135 NodeStatus::Running => NodeVisualStatus::Running,
136 NodeStatus::Completed => NodeVisualStatus::Completed,
137 NodeStatus::Failed => NodeVisualStatus::Failed {
138 error: error.cloned().unwrap_or_default(),
139 },
140 NodeStatus::Skipped => NodeVisualStatus::Skipped,
141 }
142}
143
144impl WorkflowViewState {
145 pub fn set_workflow(&mut self, def: WorkflowDef) {
147 self.workflow_def = Some(def.clone());
148 self.layout = compute_layout(&def);
149 self.visible = true;
150 }
151
152 pub fn update_context(&mut self, context: WorkflowContext) {
154 self.context = Some(context);
155 }
156
157 pub fn load_recent_instances(project_dir: Option<&PathBuf>) -> Vec<WorkflowContext> {
160 let persistence = WorkflowPersistence::new(project_dir);
161 if let Ok(contexts) = persistence.list() {
162 let mut sorted = contexts;
164 sorted.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
165 let running: Vec<_> = sorted
167 .iter()
168 .filter(|c| {
169 matches!(
170 c.status,
171 matrixcode_core::workflow::WorkflowStatus::Running
172 | matrixcode_core::workflow::WorkflowStatus::Paused
173 )
174 })
175 .cloned()
176 .collect();
177 if running.is_empty() {
178 sorted.into_iter().take(5).collect()
179 } else {
180 running
181 }
182 } else {
183 Vec::new()
184 }
185 }
186
187 pub fn load_workflow_def(
189 project_dir: Option<&PathBuf>,
190 workflow_id: &str,
191 ) -> Option<WorkflowDef> {
192 let registry = WorkflowRegistry::new(project_dir);
193 if let Some(info) = registry.get(workflow_id) {
194 matrixcode_core::workflow::parse_workflow_from_file(&info.path).ok()
195 } else {
196 None
197 }
198 }
199
200 pub fn load_most_recent(&mut self, project_dir: Option<&PathBuf>) {
202 let instances = Self::load_recent_instances(project_dir);
203 if let Some(ctx) = instances.first() {
204 if let Some(def) = Self::load_workflow_def(project_dir, &ctx.workflow_id) {
206 self.set_workflow(def);
207 self.update_context(ctx.clone());
208 }
209 }
210 }
211
212 pub fn get_node_status(&self, node_id: &str) -> NodeVisualStatus {
214 if let Some(ctx) = &self.context
215 && let Some(exec) = ctx.node_executions.get(node_id)
216 {
217 return to_visual_status(&exec.status, exec.error.as_ref());
218 }
219 NodeVisualStatus::Pending
220 }
221
222 pub fn progress(&self) -> (usize, usize) {
224 if let Some(ctx) = &self.context {
225 let total = self.layout.node_positions.len();
226 let completed = ctx.execution_path.len();
227 return (completed, total);
228 }
229 (0, self.layout.node_positions.len())
230 }
231
232 pub fn advance_spinner(&mut self) {
234 self.spinner_frame = (self.spinner_frame + 1) % 10;
235 }
236
237 pub fn spinner_char(&self) -> char {
239 const SPINNER: &[char] = &['⠁', '⠃', '⠇', '⡇', '⣇', '⣧', '⣷', '⣿', '⢟', '⠟'];
240 SPINNER[self.spinner_frame % SPINNER.len()]
241 }
242}
243
244fn compute_layout(def: &WorkflowDef) -> DagLayout {
246 let mut layout = DagLayout::default();
247
248 let mut visited: HashMap<String, bool> = HashMap::new();
250 let mut layers: Vec<Vec<String>> = Vec::new();
251
252 let start_node = def
254 .nodes
255 .iter()
256 .find(|n| n.node_type == NodeType::Start)
257 .map(|n| n.id.clone());
258
259 if let Some(start) = start_node {
260 let mut current_layer = vec![start.clone()];
262 visited.insert(start.clone(), true);
263
264 while !current_layer.is_empty() {
265 layers.push(current_layer.clone());
266 let mut next_layer = Vec::new();
267
268 for node_id in ¤t_layer {
269 for edge in &def.edges {
271 if &edge.from == node_id && !visited.contains_key(&edge.to) {
272 visited.insert(edge.to.clone(), true);
273 next_layer.push(edge.to.clone());
274 }
275 }
276 }
277
278 current_layer = next_layer;
279 }
280 }
281
282 for (row, layer) in layers.iter().enumerate() {
284 for (col, node_id) in layer.iter().enumerate() {
285 layout.node_positions.insert(node_id.clone(), (row, col));
286 }
287 }
288
289 layout.layers = layers.clone();
290 layout.height = layers.len();
291 layout.width = layers.iter().map(|l| l.len()).max().unwrap_or(0);
292
293 for edge in &def.edges {
295 layout.edges.push(EdgeInfo {
296 from: edge.from.clone(),
297 to: edge.to.clone(),
298 condition: edge.condition.clone(),
299 });
300 }
301
302 layout
303}