edda-framework 0.5.0__py3-none-any.whl → 0.7.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,11 +5,14 @@ UI components for generating interactive Mermaid diagrams.
5
5
  import json
6
6
  from typing import Any
7
7
 
8
+ from edda.viewer_ui.theme import get_edge_color, get_mermaid_node_style, get_mermaid_style
8
9
  from edda.visualizer.ast_analyzer import WorkflowAnalyzer
9
10
  from edda.visualizer.mermaid_generator import MermaidGenerator
10
11
 
11
12
 
12
- def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]) -> str:
13
+ def generate_interactive_mermaid(
14
+ instance_id: str, history: list[dict[str, Any]], is_dark: bool = False
15
+ ) -> str:
13
16
  """
14
17
  Generate interactive Mermaid diagram from execution history.
15
18
 
@@ -22,6 +25,7 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
22
25
  Args:
23
26
  instance_id: Workflow instance ID
24
27
  history: List of execution activity dictionaries
28
+ is_dark: Whether dark mode is enabled
25
29
 
26
30
  Returns:
27
31
  Mermaid flowchart diagram code with embedded click events
@@ -41,6 +45,20 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
41
45
  activity_occurrences: dict[str, list[str]] = {} # Track activity name -> list of activity_ids
42
46
  activity_index = 0
43
47
 
48
+ # Status icon mapping
49
+ status_icons = {
50
+ "completed": "✅",
51
+ "running": "⏳",
52
+ "failed": "❌",
53
+ "waiting_for_event": "⏸️",
54
+ "waiting_for_timer": "⏱️",
55
+ "cancelled": "🚫",
56
+ "compensated": "🔄",
57
+ "compensating": "🔄",
58
+ "compensation_failed": "⚠️",
59
+ "event_received": "📨",
60
+ }
61
+
44
62
  for activity_data in history:
45
63
  activity_id = activity_data.get("activity_id")
46
64
  if not activity_id:
@@ -64,33 +82,13 @@ def generate_interactive_mermaid(instance_id: str, history: list[dict[str, Any]]
64
82
  label_suffix = f" ({occurrence_count}x)" if occurrence_count >= 2 else ""
65
83
 
66
84
  # Node label with status icon
67
- if status == "completed":
68
- label = f" {activity}{label_suffix}"
69
- style_color = "fill:#d4edda,stroke:#28a745,stroke-width:2px"
70
- elif status == "running":
71
- label = f"⏳ {activity}{label_suffix}"
72
- style_color = "fill:#fff3cd,stroke:#ffc107,stroke-width:2px"
73
- elif status == "failed":
74
- label = f"❌ {activity}{label_suffix}"
75
- style_color = "fill:#f8d7da,stroke:#dc3545,stroke-width:2px"
76
- elif status == "waiting_for_event":
77
- label = f"⏸️ {activity}{label_suffix}"
78
- style_color = "fill:#e7f3ff,stroke:#0066cc,stroke-width:2px"
79
- elif status == "cancelled":
80
- label = f"🚫 {activity}{label_suffix}"
81
- style_color = "fill:#fff4e6,stroke:#ff9800,stroke-width:2px"
82
- elif status == "compensated":
83
- label = f"🔄 {activity}{label_suffix}"
84
- style_color = "fill:#ffe6f0,stroke:#e91e63,stroke-width:2px,stroke-dasharray:5"
85
- elif status == "compensation_failed":
86
- label = f"⚠️ {activity}{label_suffix}"
87
- style_color = "fill:#ffcccc,stroke:#cc0000,stroke-width:2px"
88
- elif status == "event_received":
89
- label = f"📨 {activity}{label_suffix}"
90
- style_color = "fill:#e6f7ff,stroke:#1890ff,stroke-width:2px"
91
- else:
92
- label = f"{activity}{label_suffix}"
93
- style_color = "fill:#f0f0f0,stroke:#666,stroke-width:2px"
85
+ icon = status_icons.get(status, "")
86
+ label = f"{icon} {activity}{label_suffix}" if icon else f"{activity}{label_suffix}"
87
+
88
+ # Get themed style colors
89
+ style_color = get_mermaid_style(status, is_dark)
90
+ if status == "compensated":
91
+ style_color += ",stroke-dasharray:5"
94
92
 
95
93
  # Node definition
96
94
  lines.append(f' {node_id}["{label}"]')
@@ -133,6 +131,7 @@ class HybridMermaidGenerator(MermaidGenerator):
133
131
  compensations: dict[str, dict[str, Any]] | None = None,
134
132
  workflow_status: str = "running",
135
133
  activity_status_map: dict[str, str] | None = None,
134
+ is_dark: bool = False,
136
135
  ):
137
136
  """
138
137
  Initialize hybrid Mermaid generator.
@@ -143,6 +142,7 @@ class HybridMermaidGenerator(MermaidGenerator):
143
142
  compensations: Optional mapping of activity_id -> compensation info
144
143
  workflow_status: Status of the workflow instance (running, completed, failed, etc.)
145
144
  activity_status_map: Optional mapping of activity name to status (completed, failed, etc.)
145
+ is_dark: Whether dark mode is enabled
146
146
  """
147
147
  super().__init__()
148
148
  self.instance_id = instance_id
@@ -150,6 +150,7 @@ class HybridMermaidGenerator(MermaidGenerator):
150
150
  self.compensations = compensations or {}
151
151
  self.workflow_status = workflow_status
152
152
  self.activity_status_map = activity_status_map or {}
153
+ self.is_dark = is_dark
153
154
  self.activity_id_map: dict[str, str] = {} # Map activity name to activity_id for clicks
154
155
  self.activity_execution_counts: dict[str, int] = {} # Map activity name to execution count
155
156
  self.edge_counter = 0 # Track edge indices for linkStyle
@@ -200,9 +201,8 @@ class HybridMermaidGenerator(MermaidGenerator):
200
201
  # Add a visual separator (compensation execution section)
201
202
  comp_start_id = self._next_node_id()
202
203
  self.lines.append(f" {comp_start_id}[Compensation Execution]")
203
- self.lines.append(
204
- f" style {comp_start_id} fill:#fff3cd,stroke:#ffc107,stroke-width:2px"
205
- )
204
+ comp_header_style = get_mermaid_style("running", self.is_dark)
205
+ self.lines.append(f" style {comp_start_id} {comp_header_style}")
206
206
  self.lines.append(f" {end_id} -.->|rollback| {comp_start_id}")
207
207
  self.edge_counter += 1
208
208
 
@@ -217,9 +217,8 @@ class HybridMermaidGenerator(MermaidGenerator):
217
217
  label = f"🔄 {comp_name}"
218
218
 
219
219
  self.lines.append(f' {comp_node_id}["{label}"]')
220
- self.lines.append(
221
- f" style {comp_node_id} fill:#f8d7da,stroke:#dc3545,stroke-width:3px"
222
- )
220
+ comp_node_style = get_mermaid_style("compensating", self.is_dark)
221
+ self.lines.append(f" style {comp_node_id} {comp_node_style},stroke-width:3px")
223
222
 
224
223
  # Add click event for compensation activity
225
224
  self.lines.append(
@@ -233,10 +232,11 @@ class HybridMermaidGenerator(MermaidGenerator):
233
232
 
234
233
  prev_comp_id = comp_node_id
235
234
 
236
- # Add linkStyle for executed edges (green color)
235
+ # Add linkStyle for executed edges
237
236
  if self.executed_edges:
238
237
  edge_indices = ",".join(str(i) for i in self.executed_edges)
239
- self.lines.append(f" linkStyle {edge_indices} stroke:#28a745,stroke-width:3px")
238
+ edge_color = get_edge_color("executed", self.is_dark)
239
+ self.lines.append(f" linkStyle {edge_indices} stroke:{edge_color},stroke-width:3px")
240
240
 
241
241
  return "\n".join(self.lines)
242
242
 
@@ -296,21 +296,22 @@ class HybridMermaidGenerator(MermaidGenerator):
296
296
  if has_compensation:
297
297
  label += " ⚠"
298
298
 
299
- # Style based on status
299
+ # Style based on status using theme colors
300
300
  if status == "completed":
301
- style_color = "fill:#d4edda,stroke:#28a745,stroke-width:3px" # Green
301
+ style_color = get_mermaid_style("completed", self.is_dark) + ",stroke-width:3px"
302
302
  elif status == "failed":
303
- style_color = "fill:#f8d7da,stroke:#dc3545,stroke-width:3px" # Red
303
+ style_color = get_mermaid_style("failed", self.is_dark) + ",stroke-width:3px"
304
304
  elif status == "compensated":
305
305
  style_color = (
306
- "fill:#ffe6f0,stroke:#e91e63,stroke-width:3px,stroke-dasharray:5" # Pink
306
+ get_mermaid_style("compensating", self.is_dark)
307
+ + ",stroke-width:3px,stroke-dasharray:5"
307
308
  )
308
309
  elif status is not None:
309
- # Other executed statuses
310
- style_color = "fill:#fff3cd,stroke:#ffc107,stroke-width:3px" # Yellow
310
+ # Other executed statuses (running, waiting)
311
+ style_color = get_mermaid_style("running", self.is_dark) + ",stroke-width:3px"
311
312
  else:
312
313
  # Not executed
313
- style_color = "fill:#f5f5f5,stroke:#ccc,stroke-width:1px" # Gray
314
+ style_color = get_mermaid_style("not_executed", self.is_dark)
314
315
 
315
316
  self.lines.append(f' {node_id}["{label}"]')
316
317
  self.lines.append(f" style {node_id} {style_color}")
@@ -338,7 +339,8 @@ class HybridMermaidGenerator(MermaidGenerator):
338
339
  func_name = step.get("activity_name", step.get("function", "unknown"))
339
340
  self.lines.append(f" {node_id}[register_compensation:<br/>{func_name}]")
340
341
  self.lines.append(f" {current_id} --> {node_id}")
341
- self.lines.append(f" style {node_id} fill:#ffe6e6")
342
+ comp_reg_style = get_mermaid_style("compensating", self.is_dark)
343
+ self.lines.append(f" style {node_id} {comp_reg_style}")
342
344
 
343
345
  # Track compensation for reverse path
344
346
  self.compensation_nodes.append((current_id, node_id))
@@ -357,7 +359,8 @@ class HybridMermaidGenerator(MermaidGenerator):
357
359
 
358
360
  self.lines.append(f" {node_id}{{{{{label}}}}}")
359
361
  self.lines.append(f" {current_id} --> {node_id}")
360
- self.lines.append(f" style {node_id} fill:#fff4e6")
362
+ wait_event_style = get_mermaid_style("waiting_event", self.is_dark)
363
+ self.lines.append(f" style {node_id} {wait_event_style}")
361
364
  current_id = node_id
362
365
 
363
366
  elif step_type == "condition":
@@ -446,13 +449,15 @@ class HybridMermaidGenerator(MermaidGenerator):
446
449
  self.executed_edges.append(self.edge_counter)
447
450
  self.edge_counter += 1
448
451
 
449
- self.lines.append(f" style {cond_id} fill:#fff3e0,stroke:#ff9800,stroke-width:2px")
452
+ condition_style = get_mermaid_node_style("condition", self.is_dark)
453
+ self.lines.append(f" style {cond_id} {condition_style}")
450
454
 
451
455
  # Create merge node with invisible/minimal label
452
456
  merge_id = self._next_node_id()
453
457
  # Use a minimal circle node for merging (instead of showing "N4")
454
458
  self.lines.append(f" {merge_id}(( ))") # Small empty circle
455
- self.lines.append(f" style {merge_id} fill:#ffffff,stroke:#ddd,stroke-width:1px")
459
+ merge_style = get_mermaid_node_style("merge", self.is_dark)
460
+ self.lines.append(f" style {merge_id} {merge_style}")
456
461
 
457
462
  # If branch
458
463
  if if_branch:
@@ -555,21 +560,22 @@ class HybridMermaidGenerator(MermaidGenerator):
555
560
  else:
556
561
  label = f"{label_prefix}{func_name}"
557
562
 
558
- # Style based on status
563
+ # Style based on status using theme colors
559
564
  if status == "completed":
560
- style_color = "fill:#d4edda,stroke:#28a745,stroke-width:3px" # Green
565
+ style_color = get_mermaid_style("completed", self.is_dark) + ",stroke-width:3px"
561
566
  elif status == "failed":
562
- style_color = "fill:#f8d7da,stroke:#dc3545,stroke-width:3px" # Red
567
+ style_color = get_mermaid_style("failed", self.is_dark) + ",stroke-width:3px"
563
568
  elif status == "compensated":
564
569
  style_color = (
565
- "fill:#ffe6f0,stroke:#e91e63,stroke-width:3px,stroke-dasharray:5" # Pink
570
+ get_mermaid_style("compensating", self.is_dark)
571
+ + ",stroke-width:3px,stroke-dasharray:5"
566
572
  )
567
573
  elif status is not None:
568
- # Other executed statuses
569
- style_color = "fill:#fff3cd,stroke:#ffc107,stroke-width:3px" # Yellow
574
+ # Other executed statuses (running, waiting)
575
+ style_color = get_mermaid_style("running", self.is_dark) + ",stroke-width:3px"
570
576
  else:
571
577
  # Not executed
572
- style_color = "fill:#f5f5f5,stroke:#ccc,stroke-width:1px" # Gray
578
+ style_color = get_mermaid_style("not_executed", self.is_dark)
573
579
 
574
580
  self.lines.append(f' {node_id}["{label}"]')
575
581
  self.lines.append(f" style {node_id} {style_color}")
@@ -732,7 +738,8 @@ class HybridMermaidGenerator(MermaidGenerator):
732
738
  self.lines.append(f' {loop_id}["{label}"]')
733
739
  self.lines.append(f" {prev_id} --> {loop_id}")
734
740
  self.edge_counter += 1 # Edge from prev to loop
735
- self.lines.append(f" style {loop_id} fill:#fff0f0")
741
+ loop_style = get_mermaid_node_style("loop", self.is_dark)
742
+ self.lines.append(f" style {loop_id} {loop_style}")
736
743
 
737
744
  # Check if loop body contains executed activities
738
745
  body = loop.get("body", [])
@@ -750,7 +757,8 @@ class HybridMermaidGenerator(MermaidGenerator):
750
757
  # Create exit node as small empty circle (instead of labeled node)
751
758
  exit_id = self._next_node_id()
752
759
  self.lines.append(f" {exit_id}(( ))") # Small empty circle
753
- self.lines.append(f" style {exit_id} fill:#ffffff,stroke:#ddd,stroke-width:1px")
760
+ merge_style = get_mermaid_node_style("merge", self.is_dark)
761
+ self.lines.append(f" style {exit_id} {merge_style}")
754
762
  self.lines.append(f" {loop_id} -->|exit| {exit_id}")
755
763
  self.edge_counter += 1
756
764
 
@@ -786,12 +794,14 @@ class HybridMermaidGenerator(MermaidGenerator):
786
794
  self.lines.append(f" {match_id}{{{{match {subject}}}}}")
787
795
  self.lines.append(f" {prev_id} --> {match_id}")
788
796
  self.edge_counter += 1
789
- self.lines.append(f" style {match_id} fill:#e8f5e9,stroke:#4caf50,stroke-width:2px")
797
+ match_style = get_mermaid_node_style("match", self.is_dark)
798
+ self.lines.append(f" style {match_id} {match_style}")
790
799
 
791
800
  # Create merge node
792
801
  merge_id = self._next_node_id()
793
802
  self.lines.append(f" {merge_id}(( ))") # Small empty circle for merge
794
- self.lines.append(f" style {merge_id} fill:#ffffff,stroke:#ddd,stroke-width:1px")
803
+ merge_style = get_mermaid_node_style("merge", self.is_dark)
804
+ self.lines.append(f" style {merge_id} {merge_style}")
795
805
 
796
806
  # Process each case
797
807
  cases = match.get("cases", [])
@@ -833,9 +843,8 @@ class HybridMermaidGenerator(MermaidGenerator):
833
843
  self.executed_edges.append(self.edge_counter)
834
844
  self.edge_counter += 1
835
845
 
836
- self.lines.append(
837
- f" style {case_start_id} fill:#fff,stroke:#999,stroke-width:1px"
838
- )
846
+ case_start_style = get_mermaid_node_style("merge", self.is_dark)
847
+ self.lines.append(f" style {case_start_id} {case_start_style}")
839
848
 
840
849
  # Generate body steps starting from case_start_id
841
850
  case_end = self._generate_steps(body, case_start_id)
@@ -875,12 +884,14 @@ class HybridMermaidGenerator(MermaidGenerator):
875
884
  self.lines.append(f" {decision_id}{{{{if-elif-else}}}}")
876
885
  self.lines.append(f" {prev_id} --> {decision_id}")
877
886
  self.edge_counter += 1
878
- self.lines.append(f" style {decision_id} fill:#e8f5e9,stroke:#4caf50,stroke-width:2px")
887
+ decision_style = get_mermaid_node_style("condition", self.is_dark)
888
+ self.lines.append(f" style {decision_id} {decision_style}")
879
889
 
880
890
  # Create merge node
881
891
  merge_id = self._next_node_id()
882
892
  self.lines.append(f" {merge_id}(( ))") # Small empty circle for merge
883
- self.lines.append(f" style {merge_id} fill:#ffffff,stroke:#ddd,stroke-width:1px")
893
+ merge_style = get_mermaid_node_style("merge", self.is_dark)
894
+ self.lines.append(f" style {merge_id} {merge_style}")
884
895
 
885
896
  # Process each branch
886
897
  branches = multi_cond.get("branches", [])
@@ -950,9 +961,8 @@ class HybridMermaidGenerator(MermaidGenerator):
950
961
  self.executed_edges.append(self.edge_counter)
951
962
  self.edge_counter += 1
952
963
 
953
- self.lines.append(
954
- f" style {branch_start_id} fill:#fff,stroke:#999,stroke-width:1px"
955
- )
964
+ branch_start_style = get_mermaid_node_style("merge", self.is_dark)
965
+ self.lines.append(f" style {branch_start_id} {branch_start_style}")
956
966
 
957
967
  # Generate body steps starting from branch_start_id
958
968
  branch_end = self._generate_steps(body, branch_start_id)
@@ -1006,6 +1016,7 @@ def generate_hybrid_mermaid(
1006
1016
  source_code: str,
1007
1017
  compensations: dict[str, dict[str, Any]] | None = None,
1008
1018
  workflow_status: str = "running",
1019
+ is_dark: bool = False,
1009
1020
  ) -> str:
1010
1021
  """
1011
1022
  Generate hybrid Mermaid diagram combining static analysis and execution history.
@@ -1017,6 +1028,7 @@ def generate_hybrid_mermaid(
1017
1028
  source_code: Source code of the workflow function
1018
1029
  compensations: Optional mapping of activity_id -> compensation info
1019
1030
  workflow_status: Status of the workflow instance (running, completed, failed, etc.)
1031
+ is_dark: Whether dark mode is enabled
1020
1032
 
1021
1033
  Returns:
1022
1034
  Mermaid flowchart diagram code with execution highlighting
@@ -1030,7 +1042,7 @@ def generate_hybrid_mermaid(
1030
1042
 
1031
1043
  if not workflows:
1032
1044
  # Fallback to history-only diagram
1033
- return generate_interactive_mermaid(instance_id, history)
1045
+ return generate_interactive_mermaid(instance_id, history, is_dark)
1034
1046
 
1035
1047
  workflow_structure = workflows[0]
1036
1048
 
@@ -1079,6 +1091,7 @@ def generate_hybrid_mermaid(
1079
1091
  compensations,
1080
1092
  workflow_status,
1081
1093
  activity_status_map,
1094
+ is_dark,
1082
1095
  )
1083
1096
  generator.activity_id_map = activity_id_map
1084
1097
  generator.activity_execution_counts = activity_execution_counts
@@ -1089,7 +1102,7 @@ def generate_hybrid_mermaid(
1089
1102
  except Exception as e:
1090
1103
  # Fallback to history-only diagram on any error
1091
1104
  print(f"Warning: Hybrid diagram generation failed, falling back to history-only: {e}")
1092
- return generate_interactive_mermaid(instance_id, history)
1105
+ return generate_interactive_mermaid(instance_id, history, is_dark)
1093
1106
 
1094
1107
 
1095
1108
  def format_json_for_display(data: Any) -> str:
@@ -5,7 +5,8 @@ Data service for retrieving workflow instance data from storage.
5
5
  import inspect
6
6
  import json
7
7
  import logging
8
- from typing import Any
8
+ from datetime import datetime
9
+ from typing import Any, cast
9
10
 
10
11
  from edda.pydantic_utils import is_pydantic_model
11
12
  from edda.storage.protocol import StorageProtocol
@@ -36,8 +37,46 @@ class WorkflowDataService:
36
37
  Returns:
37
38
  List of workflow instance dictionaries
38
39
  """
39
- instances = await self.storage.list_instances(limit=limit)
40
- return instances
40
+ result = await self.storage.list_instances(limit=limit)
41
+ return cast(list[dict[str, Any]], result["instances"])
42
+
43
+ async def get_instances_paginated(
44
+ self,
45
+ page_size: int = 20,
46
+ page_token: str | None = None,
47
+ status_filter: str | None = None,
48
+ search_query: str | None = None,
49
+ started_after: datetime | None = None,
50
+ started_before: datetime | None = None,
51
+ ) -> dict[str, Any]:
52
+ """
53
+ Get workflow instances with cursor-based pagination and filtering.
54
+
55
+ Args:
56
+ page_size: Number of instances per page (10, 20, or 50)
57
+ page_token: Cursor for pagination (from previous response)
58
+ status_filter: Filter by status (e.g., "running", "completed", "failed")
59
+ search_query: Search by workflow name or instance ID (partial match)
60
+ started_after: Filter instances started after this datetime
61
+ started_before: Filter instances started before this datetime
62
+
63
+ Returns:
64
+ Dictionary containing:
65
+ - instances: List of workflow instances
66
+ - next_page_token: Cursor for next page, or None
67
+ - has_more: Boolean indicating if more pages exist
68
+ """
69
+ # Use search_query for both workflow_name_filter and instance_id_filter
70
+ result = await self.storage.list_instances(
71
+ limit=page_size,
72
+ page_token=page_token,
73
+ status_filter=status_filter,
74
+ workflow_name_filter=search_query,
75
+ instance_id_filter=search_query,
76
+ started_after=started_after,
77
+ started_before=started_before,
78
+ )
79
+ return result
41
80
 
42
81
  async def get_workflow_compensations(self, instance_id: str) -> dict[str, dict[str, Any]]:
43
82
  """
@@ -0,0 +1,200 @@
1
+ """
2
+ Theme configuration for Edda Workflow Viewer.
3
+
4
+ Provides centralized color palette definitions and helper functions
5
+ for light and dark mode support.
6
+ """
7
+
8
+ from typing import Any, cast
9
+
10
+ # Tailwind CSS compatible color palette
11
+ COLORS: dict[str, dict[str, Any]] = {
12
+ "light": {
13
+ # Background colors
14
+ "bg_page": "#FFFFFF",
15
+ "bg_surface": "#F8FAFC", # Slate 50
16
+ "bg_elevated": "#FFFFFF",
17
+ "border": "#E2E8F0", # Slate 200
18
+ # Text colors
19
+ "text_primary": "#0F172A", # Slate 900
20
+ "text_secondary": "#64748B", # Slate 500
21
+ "text_muted": "#94A3B8", # Slate 400
22
+ # Status colors (bg, stroke, text)
23
+ "completed": {"bg": "#ECFDF5", "stroke": "#10B981", "text": "#065F46"},
24
+ "running": {"bg": "#FEF3C7", "stroke": "#F59E0B", "text": "#92400E"},
25
+ "failed": {"bg": "#FEE2E2", "stroke": "#EF4444", "text": "#991B1B"},
26
+ "waiting_event": {"bg": "#DBEAFE", "stroke": "#3B82F6", "text": "#1E40AF"},
27
+ "waiting_timer": {"bg": "#E0F2FE", "stroke": "#0EA5E9", "text": "#075985"},
28
+ "cancelled": {"bg": "#FFEDD5", "stroke": "#F97316", "text": "#9A3412"},
29
+ "compensating": {"bg": "#F3E8FF", "stroke": "#A855F7", "text": "#6B21A8"},
30
+ "not_executed": {"bg": "#F1F5F9", "stroke": "#CBD5E1", "text": "#64748B"},
31
+ "event_received": {"bg": "#CFFAFE", "stroke": "#06B6D4", "text": "#0E7490"},
32
+ "compensation_failed": {"bg": "#FEE2E2", "stroke": "#B91C1C", "text": "#7F1D1D"},
33
+ # Mermaid diagram specific
34
+ "condition": {"bg": "#FEF3C7", "stroke": "#F59E0B"},
35
+ "loop": {"bg": "#FDF4FF", "stroke": "#D946EF"},
36
+ "match": {"bg": "#ECFDF5", "stroke": "#10B981"},
37
+ "merge": {"bg": "#FFFFFF", "stroke": "#E2E8F0"},
38
+ "edge_executed": "#10B981",
39
+ "edge_not_executed": "#CBD5E1",
40
+ "edge_compensation": "#A855F7",
41
+ },
42
+ "dark": {
43
+ # Background colors
44
+ "bg_page": "#0F172A", # Slate 900
45
+ "bg_surface": "#1E293B", # Slate 800
46
+ "bg_elevated": "#334155", # Slate 700
47
+ "border": "#475569", # Slate 600
48
+ # Text colors
49
+ "text_primary": "#F1F5F9", # Slate 100
50
+ "text_secondary": "#94A3B8", # Slate 400
51
+ "text_muted": "#64748B", # Slate 500
52
+ # Status colors (bg, stroke, text)
53
+ "completed": {"bg": "#064E3B", "stroke": "#34D399", "text": "#A7F3D0"},
54
+ "running": {"bg": "#78350F", "stroke": "#FBBF24", "text": "#FDE68A"},
55
+ "failed": {"bg": "#7F1D1D", "stroke": "#F87171", "text": "#FECACA"},
56
+ "waiting_event": {"bg": "#1E3A8A", "stroke": "#60A5FA", "text": "#BFDBFE"},
57
+ "waiting_timer": {"bg": "#0C4A6E", "stroke": "#38BDF8", "text": "#BAE6FD"},
58
+ "cancelled": {"bg": "#7C2D12", "stroke": "#FB923C", "text": "#FED7AA"},
59
+ "compensating": {"bg": "#581C87", "stroke": "#C084FC", "text": "#E9D5FF"},
60
+ "not_executed": {"bg": "#334155", "stroke": "#64748B", "text": "#94A3B8"},
61
+ "event_received": {"bg": "#164E63", "stroke": "#22D3EE", "text": "#A5F3FC"},
62
+ "compensation_failed": {"bg": "#7F1D1D", "stroke": "#FCA5A5", "text": "#FECACA"},
63
+ # Mermaid diagram specific
64
+ "condition": {"bg": "#78350F", "stroke": "#FBBF24"},
65
+ "loop": {"bg": "#4C1D95", "stroke": "#E879F9"},
66
+ "match": {"bg": "#064E3B", "stroke": "#34D399"},
67
+ "merge": {"bg": "#1E293B", "stroke": "#475569"},
68
+ "edge_executed": "#34D399",
69
+ "edge_not_executed": "#64748B",
70
+ "edge_compensation": "#C084FC",
71
+ },
72
+ }
73
+
74
+
75
+ def get_status_color(status: str, is_dark: bool) -> dict[str, str]:
76
+ """
77
+ Get color configuration for a workflow status.
78
+
79
+ Args:
80
+ status: Status name (e.g., "completed", "running", "failed")
81
+ is_dark: Whether dark mode is enabled
82
+
83
+ Returns:
84
+ Dictionary with 'bg', 'stroke', and 'text' colors
85
+ """
86
+ theme = "dark" if is_dark else "light"
87
+ status_key = status.lower().replace(" ", "_").replace("-", "_")
88
+
89
+ # Handle special status mappings
90
+ status_mapping = {
91
+ "waiting": "waiting_event",
92
+ "waiting_for_event": "waiting_event",
93
+ "waiting_for_timer": "waiting_timer",
94
+ "compensated": "compensating",
95
+ }
96
+ status_key = status_mapping.get(status_key, status_key)
97
+
98
+ return cast(dict[str, str], COLORS[theme].get(status_key, COLORS[theme]["not_executed"]))
99
+
100
+
101
+ def get_mermaid_style(status: str, is_dark: bool) -> str:
102
+ """
103
+ Get Mermaid style string for a status.
104
+
105
+ Args:
106
+ status: Status name
107
+ is_dark: Whether dark mode is enabled
108
+
109
+ Returns:
110
+ Mermaid style string (e.g., "fill:#ECFDF5,stroke:#10B981,stroke-width:2px")
111
+ """
112
+ colors = get_status_color(status, is_dark)
113
+ return f"fill:{colors['bg']},stroke:{colors['stroke']},stroke-width:2px"
114
+
115
+
116
+ def get_mermaid_node_style(node_type: str, is_dark: bool) -> str:
117
+ """
118
+ Get Mermaid style for structural nodes (condition, loop, merge).
119
+
120
+ Args:
121
+ node_type: Node type (e.g., "condition", "loop", "merge")
122
+ is_dark: Whether dark mode is enabled
123
+
124
+ Returns:
125
+ Mermaid style string
126
+ """
127
+ theme = "dark" if is_dark else "light"
128
+ colors = COLORS[theme].get(node_type, COLORS[theme]["merge"])
129
+ stroke_width = "1px" if node_type == "merge" else "2px"
130
+ return f"fill:{colors['bg']},stroke:{colors['stroke']},stroke-width:{stroke_width}"
131
+
132
+
133
+ def get_edge_color(edge_type: str, is_dark: bool) -> str:
134
+ """
135
+ Get edge (arrow) color for Mermaid diagrams.
136
+
137
+ Args:
138
+ edge_type: Edge type ("executed", "not_executed", "compensation")
139
+ is_dark: Whether dark mode is enabled
140
+
141
+ Returns:
142
+ Color hex code
143
+ """
144
+ theme = "dark" if is_dark else "light"
145
+ key = f"edge_{edge_type}"
146
+ return cast(str, COLORS[theme].get(key, COLORS[theme]["edge_not_executed"]))
147
+
148
+
149
+ # Tailwind CSS class mappings for UI components
150
+ # Note: Background colors are controlled via CSS in app.py for proper dark mode support
151
+ # NiceGUI adds 'dark' class to body, but Tailwind's dark: prefix expects it on html
152
+ TAILWIND_CLASSES = {
153
+ "card": "border", # Background handled by CSS
154
+ "card_hover": "", # Hover handled by CSS
155
+ "surface": "", # Background handled by CSS
156
+ "text_primary": "text-slate-900 dark:text-slate-100",
157
+ "text_secondary": "text-slate-500 dark:text-slate-400",
158
+ "text_muted": "text-slate-400 dark:text-slate-500",
159
+ "border": "border-slate-200 dark:border-slate-700",
160
+ "input": "border-slate-300 dark:border-slate-600", # Background handled by CSS
161
+ "code_block": "border", # Background handled by CSS
162
+ }
163
+
164
+ # Status badge Tailwind classes
165
+ STATUS_BADGE_CLASSES = {
166
+ "completed": ("bg-emerald-50 text-emerald-700 dark:bg-emerald-900/50 dark:text-emerald-300"),
167
+ "running": "bg-amber-50 text-amber-700 dark:bg-amber-900/50 dark:text-amber-300",
168
+ "failed": "bg-red-50 text-red-700 dark:bg-red-900/50 dark:text-red-300",
169
+ "waiting_event": "bg-blue-50 text-blue-700 dark:bg-blue-900/50 dark:text-blue-300",
170
+ "waiting_timer": "bg-sky-50 text-sky-700 dark:bg-sky-900/50 dark:text-sky-300",
171
+ "cancelled": ("bg-orange-50 text-orange-700 dark:bg-orange-900/50 dark:text-orange-300"),
172
+ "compensating": ("bg-purple-50 text-purple-700 dark:bg-purple-900/50 dark:text-purple-300"),
173
+ "not_executed": ("bg-slate-100 text-slate-600 dark:bg-slate-700 dark:text-slate-400"),
174
+ }
175
+
176
+
177
+ def get_status_badge_classes(status: str) -> str:
178
+ """
179
+ Get Tailwind CSS classes for a status badge.
180
+
181
+ Args:
182
+ status: Status name
183
+
184
+ Returns:
185
+ Tailwind CSS class string
186
+ """
187
+ status_key = status.lower().replace(" ", "_").replace("-", "_")
188
+
189
+ # Handle special status mappings
190
+ status_mapping = {
191
+ "waiting": "waiting_event",
192
+ "waiting_for_event": "waiting_event",
193
+ "waiting_for_timer": "waiting_timer",
194
+ "compensated": "compensating",
195
+ }
196
+ status_key = status_mapping.get(status_key, status_key)
197
+
198
+ base_classes = "px-3 py-1 rounded-full text-sm font-medium"
199
+ status_classes = STATUS_BADGE_CLASSES.get(status_key, STATUS_BADGE_CLASSES["not_executed"])
200
+ return f"{base_classes} {status_classes}"