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