1use super::canvas::{DfgCanvas, TimelineCanvas};
4use super::panels::{ConformancePanel, DfgPanel, FabricPanel, KpiPanel, PatternsPanel};
5use super::Theme;
6use crate::actors::PipelineCoordinator;
7use crate::cuda::GpuStatus;
8use crate::fabric::{PipelineConfig, SectorTemplate};
9use crate::models::DFGGraph;
10use eframe::egui::{self, Color32, Pos2, Rect};
11use std::collections::HashMap;
12
13#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum ViewMode {
16 Dfg,
17 Timeline,
18 Variations,
19 Split,
20}
21
22pub struct ProcessIntelligenceApp {
24 pub theme: Theme,
26 pub coordinator: PipelineCoordinator,
28 pub dfg_canvas: DfgCanvas,
30 pub timeline_canvas: TimelineCanvas,
32 pub view_mode: ViewMode,
34 pub fabric_panel: FabricPanel,
36 pub kpi_panel: KpiPanel,
38 pub patterns_panel: PatternsPanel,
40 pub conformance_panel: ConformancePanel,
42 pub dfg_panel: DfgPanel,
44 dfg: DFGGraph,
46 partial_orders: Vec<crate::models::GpuPartialOrderTrace>,
48 frame_count: u64,
50 show_settings: bool,
52 last_sector: SectorTemplate,
54}
55
56impl Default for ProcessIntelligenceApp {
57 fn default() -> Self {
58 Self::new()
59 }
60}
61
62impl ProcessIntelligenceApp {
63 pub fn new() -> Self {
65 use crate::fabric::HealthcareConfig;
66 let initial_sector = SectorTemplate::Healthcare(HealthcareConfig::default());
67 let coordinator =
68 PipelineCoordinator::new(initial_sector.clone(), PipelineConfig::default());
69
70 let mut dfg_canvas = DfgCanvas::new();
71
72 let sector = coordinator.pipeline().sector();
74 let names: HashMap<u32, String> = sector
75 .activities()
76 .iter()
77 .enumerate()
78 .map(|(i, a)| ((i + 1) as u32, a.name.to_string()))
79 .collect();
80 dfg_canvas.set_activity_names(names.clone());
81
82 let mut timeline_canvas = TimelineCanvas::new();
83 timeline_canvas.set_activity_names(names);
84
85 Self {
86 theme: Theme::dark(),
87 coordinator,
88 dfg_canvas,
89 timeline_canvas,
90 view_mode: ViewMode::Dfg,
91 fabric_panel: FabricPanel::default(),
92 kpi_panel: KpiPanel::default(),
93 patterns_panel: PatternsPanel,
94 conformance_panel: ConformancePanel::default(),
95 dfg_panel: DfgPanel,
96 dfg: DFGGraph::default(),
97 partial_orders: Vec::new(),
98 frame_count: 0,
99 show_settings: false,
100 last_sector: initial_sector,
101 }
102 }
103
104 pub fn run(self) -> eframe::Result<()> {
106 let options = eframe::NativeOptions {
107 viewport: egui::ViewportBuilder::default()
108 .with_inner_size([1400.0, 900.0])
109 .with_min_inner_size([1000.0, 600.0])
110 .with_title("RingKernel Process Intelligence"),
111 ..Default::default()
112 };
113
114 eframe::run_native(
115 "Process Intelligence",
116 options,
117 Box::new(|cc| {
118 let app = self;
120 app.theme.apply(&cc.egui_ctx);
121 Ok(Box::new(app))
122 }),
123 )
124 }
125}
126
127impl eframe::App for ProcessIntelligenceApp {
128 fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
129 self.frame_count += 1;
130
131 if self.fabric_panel.sector != self.last_sector {
133 self.last_sector = self.fabric_panel.sector.clone();
134 self.reset_views();
135 }
136
137 if self.coordinator.stats().is_running {
139 if let Some(batch) = self.coordinator.tick() {
140 self.dfg = self.coordinator.current_dfg().clone();
142 self.partial_orders = self.coordinator.current_partial_orders().to_vec();
143
144 if let Some(conformance) = self.coordinator.current_conformance() {
146 self.conformance_panel.update(conformance.clone());
147 }
148
149 if batch.event_count > 0
152 && self.frame_count.is_multiple_of(5)
153 && !self.dfg.edges().is_empty()
154 {
155 let edge_idx = (self.frame_count as usize) % self.dfg.edges().len();
156 let edge = &self.dfg.edges()[edge_idx];
157 self.dfg_canvas.tokens.spawn(
158 batch.batch_id,
159 edge.source_activity,
160 edge.target_activity,
161 );
162 }
163 }
164
165 let kpis = self.coordinator.analytics().kpi_tracker.kpis();
167 self.kpi_panel.update(kpis);
168
169 ctx.request_repaint();
171 }
172
173 self.dfg_canvas.update_layout(&self.dfg);
175
176 egui::TopBottomPanel::top("top_panel").show(ctx, |ui| {
178 self.render_top_bar(ui);
179 });
180
181 egui::TopBottomPanel::bottom("status_bar")
183 .min_height(28.0)
184 .show(ctx, |ui| {
185 self.render_status_bar(ui);
186 });
187
188 egui::SidePanel::left("left_panel")
190 .min_width(220.0)
191 .max_width(280.0)
192 .show(ctx, |ui| {
193 egui::ScrollArea::vertical().show(ui, |ui| {
194 self.fabric_panel
195 .render(ui, &self.theme, &mut self.coordinator);
196 ui.add_space(12.0);
197 let kpis = self.coordinator.analytics().kpi_tracker.kpis();
198 self.kpi_panel.render(ui, &self.theme, kpis);
199 });
200 });
201
202 egui::SidePanel::right("right_panel")
204 .min_width(200.0)
205 .max_width(260.0)
206 .show(ctx, |ui| {
207 egui::ScrollArea::vertical().show(ui, |ui| {
208 self.patterns_panel.render(
209 ui,
210 &self.theme,
211 &self.coordinator.analytics().pattern_aggregator,
212 );
213 ui.add_space(12.0);
214 self.conformance_panel.render(ui, &self.theme);
215 ui.add_space(12.0);
216 let metrics = self.coordinator.analytics().dfg_metrics.metrics();
217 self.dfg_panel.render(ui, &self.theme, metrics);
218 });
219 });
220
221 egui::CentralPanel::default().show(ctx, |ui| {
223 self.render_canvas(ui);
224 });
225 }
226}
227
228impl ProcessIntelligenceApp {
229 fn render_top_bar(&mut self, ui: &mut egui::Ui) {
231 ui.horizontal(|ui| {
232 ui.heading(
233 egui::RichText::new("RingKernel Process Intelligence")
234 .strong()
235 .color(self.theme.accent),
236 );
237
238 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
239 if ui.button("⚙").clicked() {
241 self.show_settings = !self.show_settings;
242 }
243
244 ui.separator();
245
246 ui.selectable_value(&mut self.view_mode, ViewMode::Dfg, "DFG");
248 ui.selectable_value(&mut self.view_mode, ViewMode::Timeline, "Timeline");
249 ui.selectable_value(&mut self.view_mode, ViewMode::Variations, "Variants");
250 ui.selectable_value(&mut self.view_mode, ViewMode::Split, "Split");
251 });
252 });
253 }
254
255 fn render_status_bar(&mut self, ui: &mut egui::Ui) {
257 ui.horizontal(|ui| {
258 let stats = self.coordinator.stats();
259 let kpis = self.coordinator.analytics().kpi_tracker.kpis();
260 let gpu_stats = self.coordinator.gpu_stats();
261
262 ui.label(format!("Events: {}", stats.total_events));
263 ui.separator();
264 ui.label(format!("TPS: {}", kpis.format_throughput()));
265 ui.separator();
266
267 let (gpu_text, gpu_color) = if gpu_stats.is_gpu_active() {
269 let launches = gpu_stats.total_launches();
270 if launches > 0 {
271 (
272 format!("CUDA ({} kernels)", launches),
273 Color32::from_rgb(0, 200, 100),
274 )
275 } else {
276 ("CUDA (ready)".to_string(), Color32::from_rgb(0, 200, 100))
277 }
278 } else {
279 match self.coordinator.dfg_gpu_status() {
280 GpuStatus::CudaReady => ("CUDA (compiling)".to_string(), Color32::YELLOW),
281 GpuStatus::CudaPending => ("CUDA (init)".to_string(), Color32::YELLOW),
282 GpuStatus::CudaError => ("CUDA (err)".to_string(), Color32::RED),
283 GpuStatus::CpuFallback => {
284 ("CPU fallback".to_string(), Color32::from_rgb(100, 150, 200))
285 }
286 GpuStatus::CudaNotCompiled => (
287 "CPU (build with --features cuda)".to_string(),
288 Color32::from_rgb(180, 180, 180),
289 ),
290 }
291 };
292 ui.colored_label(gpu_color, format!("GPU: {}", gpu_text));
293
294 if gpu_stats.is_gpu_active() && gpu_stats.total_launches() > 0 {
296 let throughput = gpu_stats.throughput();
297 if throughput > 0.0 {
298 ui.label(format!("| {:.0} elem/s", throughput));
299 }
300 }
301 ui.separator();
302
303 ui.label(format!(
304 "Patterns: {}",
305 self.coordinator
306 .analytics()
307 .pattern_aggregator
308 .total_detected
309 ));
310
311 ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
312 ui.label(format!("Sector: {}", self.fabric_panel.sector.name()));
313 });
314 });
315 }
316
317 fn render_canvas(&mut self, ui: &mut egui::Ui) {
319 let rect = ui.available_rect_before_wrap();
320
321 ui.painter()
323 .rect_filled(rect, egui::Rounding::ZERO, self.theme.background);
324
325 match self.view_mode {
326 ViewMode::Dfg => {
327 self.dfg_canvas.render(ui, &self.theme, &self.dfg, rect);
328 }
329 ViewMode::Timeline => {
330 self.timeline_canvas
331 .render(ui, &self.theme, &self.partial_orders, rect);
332 }
333 ViewMode::Variations => {
334 self.render_variations(ui, rect);
335 }
336 ViewMode::Split => {
337 let mid = rect.center().x;
338 let left_rect = Rect::from_min_max(rect.min, Pos2::new(mid - 2.0, rect.max.y));
339 let right_rect = Rect::from_min_max(Pos2::new(mid + 2.0, rect.min.y), rect.max);
340
341 self.dfg_canvas
342 .render(ui, &self.theme, &self.dfg, left_rect);
343
344 ui.painter().line_segment(
346 [Pos2::new(mid, rect.top()), Pos2::new(mid, rect.bottom())],
347 egui::Stroke::new(2.0, Color32::from_rgb(38, 38, 48)),
348 );
349
350 self.timeline_canvas
351 .render(ui, &self.theme, &self.partial_orders, right_rect);
352 }
353 }
354 }
355
356 fn reset_views(&mut self) {
358 self.dfg = DFGGraph::default();
360 self.partial_orders.clear();
361
362 self.dfg_canvas.reset();
364 self.timeline_canvas.reset();
365
366 let sector = self.coordinator.pipeline().sector();
368 let names: HashMap<u32, String> = sector
369 .activities()
370 .iter()
371 .enumerate()
372 .map(|(i, a)| ((i + 1) as u32, a.name.to_string()))
373 .collect();
374 self.dfg_canvas.set_activity_names(names.clone());
375 self.timeline_canvas.set_activity_names(names);
376
377 self.conformance_panel = ConformancePanel::default();
379 }
380
381 fn render_variations(&self, ui: &mut egui::Ui, rect: Rect) {
383 let painter = ui.painter_at(rect);
384
385 let mut variations: Vec<_> = self.coordinator.variations().values().collect();
387 variations.sort_by(|a, b| b.count.cmp(&a.count));
388
389 let sector = self.coordinator.pipeline().sector();
391 let activity_names: HashMap<u32, String> = sector
392 .activities()
393 .iter()
394 .enumerate()
395 .map(|(i, a)| ((i + 1) as u32, a.name.to_string()))
396 .collect();
397
398 let activity_colors = self.get_activity_colors(&activity_names);
399
400 let total_cases: u64 = variations.iter().map(|v| v.count).sum();
401
402 if variations.is_empty() {
404 painter.text(
405 rect.center(),
406 egui::Align2::CENTER_CENTER,
407 "No process variations yet.\nStart the pipeline to collect data.",
408 egui::FontId::proportional(14.0),
409 self.theme.text_muted,
410 );
411 return;
412 }
413
414 let graph_height = (rect.height() * 0.45).min(250.0);
416 let graph_rect = Rect::from_min_size(rect.min, egui::Vec2::new(rect.width(), graph_height));
417 let chart_rect = Rect::from_min_max(
418 Pos2::new(rect.left(), rect.top() + graph_height + 10.0),
419 rect.max,
420 );
421
422 painter.line_segment(
424 [
425 Pos2::new(rect.left() + 20.0, graph_rect.bottom() + 5.0),
426 Pos2::new(rect.right() - 20.0, graph_rect.bottom() + 5.0),
427 ],
428 egui::Stroke::new(1.0, self.theme.panel_bg.linear_multiply(1.5)),
429 );
430
431 self.render_variant_graph(
433 &painter,
434 graph_rect,
435 &variations,
436 &activity_names,
437 &activity_colors,
438 total_cases,
439 );
440
441 self.render_variant_bar_chart(
443 &painter,
444 chart_rect,
445 &variations,
446 &activity_names,
447 total_cases,
448 );
449 }
450
451 fn get_activity_colors(&self, activity_names: &HashMap<u32, String>) -> HashMap<u32, Color32> {
453 let palette = [
454 Color32::from_rgb(59, 130, 246), Color32::from_rgb(34, 197, 94), Color32::from_rgb(239, 68, 68), Color32::from_rgb(168, 85, 247), Color32::from_rgb(234, 179, 8), Color32::from_rgb(236, 72, 153), Color32::from_rgb(20, 184, 166), Color32::from_rgb(249, 115, 22), ];
463
464 activity_names
465 .keys()
466 .enumerate()
467 .map(|(i, &id)| (id, palette[i % palette.len()]))
468 .collect()
469 }
470
471 fn render_variant_graph(
473 &self,
474 painter: &egui::Painter,
475 rect: Rect,
476 variations: &[&crate::actors::ProcessVariation],
477 activity_names: &HashMap<u32, String>,
478 activity_colors: &HashMap<u32, Color32>,
479 total_cases: u64,
480 ) {
481 painter.text(
483 Pos2::new(rect.left() + 20.0, rect.top() + 15.0),
484 egui::Align2::LEFT_CENTER,
485 "Process Variant Graph",
486 egui::FontId::proportional(14.0),
487 self.theme.text,
488 );
489
490 painter.text(
491 Pos2::new(rect.right() - 20.0, rect.top() + 15.0),
492 egui::Align2::RIGHT_CENTER,
493 format!("{} variants, {} cases", variations.len(), total_cases),
494 egui::FontId::proportional(10.0),
495 self.theme.text_muted,
496 );
497
498 let graph_y_start = rect.top() + 35.0;
500 let graph_height = rect.height() - 45.0;
501 let row_height = (graph_height / 5.0).min(40.0);
502 let node_radius = 14.0;
503 let margin_x = 80.0;
504 let graph_width = rect.width() - margin_x * 2.0;
505
506 for (var_idx, var) in variations.iter().take(5).enumerate() {
507 let y = graph_y_start + var_idx as f32 * row_height + row_height / 2.0;
508 let num_activities = var.activities.len().max(1);
509 let spacing = graph_width / (num_activities + 1) as f32;
510
511 let percentage = if total_cases > 0 {
512 var.count as f32 / total_cases as f32
513 } else {
514 0.0
515 };
516
517 painter.text(
519 Pos2::new(rect.left() + 10.0, y),
520 egui::Align2::LEFT_CENTER,
521 format!("#{} ({:.0}%)", var_idx + 1, percentage * 100.0),
522 egui::FontId::proportional(9.0),
523 self.theme.text_muted,
524 );
525
526 for i in 0..var.activities.len().saturating_sub(1) {
528 let x1 = rect.left() + margin_x + (i + 1) as f32 * spacing + node_radius;
529 let x2 = rect.left() + margin_x + (i + 2) as f32 * spacing - node_radius;
530
531 let edge_width = (1.0 + percentage * 4.0).min(3.0);
533
534 painter.line_segment(
535 [Pos2::new(x1, y), Pos2::new(x2, y)],
536 egui::Stroke::new(edge_width, self.theme.accent.linear_multiply(0.6)),
537 );
538
539 let arrow_size = 5.0;
541 painter.line_segment(
542 [Pos2::new(x2 - arrow_size, y - arrow_size), Pos2::new(x2, y)],
543 egui::Stroke::new(edge_width, self.theme.accent.linear_multiply(0.6)),
544 );
545 painter.line_segment(
546 [Pos2::new(x2 - arrow_size, y + arrow_size), Pos2::new(x2, y)],
547 egui::Stroke::new(edge_width, self.theme.accent.linear_multiply(0.6)),
548 );
549 }
550
551 for (i, &activity_id) in var.activities.iter().enumerate() {
553 let x = rect.left() + margin_x + (i + 1) as f32 * spacing;
554 let color = activity_colors
555 .get(&activity_id)
556 .copied()
557 .unwrap_or(self.theme.accent);
558
559 painter.circle_filled(Pos2::new(x, y), node_radius, color);
561
562 if !var.is_conformant {
564 painter.circle_stroke(
565 Pos2::new(x, y),
566 node_radius + 2.0,
567 egui::Stroke::new(2.0, self.theme.error),
568 );
569 }
570
571 let name = activity_names
573 .get(&activity_id)
574 .map(|n| n.chars().take(2).collect::<String>())
575 .unwrap_or_else(|| format!("{}", activity_id));
576
577 painter.text(
578 Pos2::new(x, y),
579 egui::Align2::CENTER_CENTER,
580 name,
581 egui::FontId::proportional(8.0),
582 Color32::WHITE,
583 );
584 }
585
586 let conf_x = rect.right() - 25.0;
588 let (conf_symbol, conf_color) = if var.is_conformant {
589 ("✓", self.theme.success)
590 } else {
591 ("✗", self.theme.error)
592 };
593 painter.text(
594 Pos2::new(conf_x, y),
595 egui::Align2::CENTER_CENTER,
596 conf_symbol,
597 egui::FontId::proportional(12.0),
598 conf_color,
599 );
600 }
601 }
602
603 fn render_variant_bar_chart(
605 &self,
606 painter: &egui::Painter,
607 rect: Rect,
608 variations: &[&crate::actors::ProcessVariation],
609 activity_names: &HashMap<u32, String>,
610 total_cases: u64,
611 ) {
612 painter.text(
614 Pos2::new(rect.left() + 20.0, rect.top() + 10.0),
615 egui::Align2::LEFT_CENTER,
616 "Variant Distribution",
617 egui::FontId::proportional(14.0),
618 self.theme.text,
619 );
620
621 let chart_y_start = rect.top() + 30.0;
622 let chart_height = rect.height() - 40.0;
623 let row_height = (chart_height / 12.0).min(28.0);
624 let bar_x_start = rect.left() + 160.0;
625 let max_bar_width = rect.width() - 240.0;
626
627 let mut cumulative = 0.0;
629
630 for (i, var) in variations.iter().take(12).enumerate() {
631 let y = chart_y_start + i as f32 * row_height + row_height / 2.0;
632
633 let percentage = if total_cases > 0 {
634 var.count as f32 / total_cases as f32
635 } else {
636 0.0
637 };
638 cumulative += percentage;
639
640 painter.text(
642 Pos2::new(rect.left() + 15.0, y),
643 egui::Align2::LEFT_CENTER,
644 format!("#{}", i + 1),
645 egui::FontId::proportional(10.0),
646 self.theme.text_muted,
647 );
648
649 painter.text(
651 Pos2::new(rect.left() + 45.0, y),
652 egui::Align2::LEFT_CENTER,
653 format!("{}", var.count),
654 egui::FontId::proportional(10.0),
655 self.theme.text,
656 );
657
658 let activity_seq: String = var
660 .activities
661 .iter()
662 .take(4)
663 .map(|id| {
664 activity_names
665 .get(id)
666 .map(|n| n.chars().take(2).collect::<String>())
667 .unwrap_or_else(|| "?".to_string())
668 })
669 .collect::<Vec<_>>()
670 .join("→");
671
672 let seq_display = if var.activities.len() > 4 {
673 format!("{}…", activity_seq)
674 } else {
675 activity_seq
676 };
677
678 painter.text(
679 Pos2::new(rect.left() + 85.0, y),
680 egui::Align2::LEFT_CENTER,
681 seq_display,
682 egui::FontId::proportional(8.0),
683 self.theme.text_muted,
684 );
685
686 let bar_width = max_bar_width * percentage;
688 let bar_rect = Rect::from_min_size(
689 Pos2::new(bar_x_start, y - row_height * 0.35),
690 egui::Vec2::new(bar_width.max(2.0), row_height * 0.7),
691 );
692
693 let bar_color = if var.is_conformant {
694 self.theme.success.linear_multiply(0.7)
695 } else {
696 self.theme.error.linear_multiply(0.7)
697 };
698 painter.rect_filled(bar_rect, egui::Rounding::same(3.0), bar_color);
699
700 painter.text(
702 Pos2::new(bar_x_start + bar_width + 5.0, y),
703 egui::Align2::LEFT_CENTER,
704 format!("{:.1}%", percentage * 100.0),
705 egui::FontId::proportional(9.0),
706 self.theme.text,
707 );
708
709 painter.text(
711 Pos2::new(rect.right() - 40.0, y),
712 egui::Align2::LEFT_CENTER,
713 format!("Σ{:.0}%", cumulative * 100.0),
714 egui::FontId::proportional(8.0),
715 self.theme.text_muted,
716 );
717
718 painter.text(
720 Pos2::new(rect.right() - 10.0, y),
721 egui::Align2::RIGHT_CENTER,
722 format!("{:.0}ms", var.avg_duration_ms),
723 egui::FontId::proportional(8.0),
724 self.theme.text_muted,
725 );
726 }
727
728 if variations.len() > 12 {
730 let y = chart_y_start + 12.0 * row_height + 5.0;
731 painter.text(
732 Pos2::new(rect.center().x, y),
733 egui::Align2::CENTER_TOP,
734 format!("... and {} more variants", variations.len() - 12),
735 egui::FontId::proportional(10.0),
736 self.theme.text_muted,
737 );
738 }
739 }
740}