ruby_llm-agents 1.0.0 → 1.2.0

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.
Files changed (152) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +9 -3
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +58 -0
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +59 -16
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +144 -20
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +13 -16
  7. data/app/controllers/ruby_llm/agents/workflows_controller.rb +279 -90
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +100 -0
  9. data/app/mailers/ruby_llm/agents/alert_mailer.rb +84 -0
  10. data/app/mailers/ruby_llm/agents/application_mailer.rb +28 -0
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +170 -20
  12. data/app/models/ruby_llm/agents/execution/scopes.rb +0 -31
  13. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -129
  14. data/app/models/ruby_llm/agents/execution.rb +50 -14
  15. data/app/models/ruby_llm/agents/tenant/budgetable.rb +277 -0
  16. data/app/models/ruby_llm/agents/tenant/configurable.rb +135 -0
  17. data/app/models/ruby_llm/agents/tenant/trackable.rb +310 -0
  18. data/app/models/ruby_llm/agents/tenant.rb +146 -0
  19. data/app/models/ruby_llm/agents/tenant_budget.rb +12 -253
  20. data/app/services/ruby_llm/agents/agent_registry.rb +18 -12
  21. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  22. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  23. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  24. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  25. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  26. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  27. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  28. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  30. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  31. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  32. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  33. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  34. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  35. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  36. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  37. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  38. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  39. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  40. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  41. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  42. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  43. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  44. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  45. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  46. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  47. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  48. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  49. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  50. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  51. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  52. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  53. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  54. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  55. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  56. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  57. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  58. data/config/routes.rb +1 -1
  59. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  60. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  61. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  62. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  65. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  66. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  67. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  68. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  69. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  70. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  71. data/lib/generators/ruby_llm_agents/multi_tenancy_generator.rb +42 -22
  72. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  73. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  74. data/lib/generators/ruby_llm_agents/templates/add_tenant_to_executions_migration.rb.tt +13 -2
  75. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  76. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  77. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  78. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  79. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  80. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  81. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  82. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  83. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  84. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  85. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  86. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  87. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  88. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  89. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  90. data/lib/generators/ruby_llm_agents/templates/create_tenant_budgets_migration.rb.tt +11 -0
  91. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +72 -0
  92. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  93. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  94. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  95. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  96. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  97. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  98. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  99. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  100. data/lib/generators/ruby_llm_agents/templates/rename_tenant_budgets_to_tenants_migration.rb.tt +34 -0
  101. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  102. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  103. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  104. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  105. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  106. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  107. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  108. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  109. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  110. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  111. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  112. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  113. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  114. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  115. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  116. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  117. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  118. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +26 -0
  119. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  120. data/lib/ruby_llm/agents/core/llm_tenant.rb +60 -60
  121. data/lib/ruby_llm/agents/core/version.rb +1 -1
  122. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  123. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +4 -2
  124. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  125. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  126. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  127. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  128. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  129. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  130. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  131. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  132. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  133. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  134. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  135. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  136. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  137. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  138. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  139. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  140. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  141. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  142. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  143. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  144. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  145. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  146. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  147. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  148. metadata +43 -6
  149. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  150. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  151. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  152. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -0,0 +1,539 @@
1
+ <%
2
+ # DSL-based workflow structure visualization
3
+ # Shows steps with support for routing (branching) and parallel groups
4
+ steps = local_assigns[:steps] || []
5
+ parallel_groups = local_assigns[:parallel_groups] || []
6
+ input_schema_fields = local_assigns[:input_schema_fields] || {}
7
+ %>
8
+
9
+ <% if steps.any? %>
10
+ <!-- Visual DSL Workflow Flow -->
11
+ <div class="py-4 overflow-x-auto">
12
+ <div class="flex items-start justify-center gap-2 min-w-max px-4">
13
+ <%
14
+ # Group steps: sequential steps, parallel groups, and wait steps
15
+ current_parallel_group = nil
16
+ grouped_items = []
17
+
18
+ steps.each do |step|
19
+ # Check if this is a wait step
20
+ if step[:type] == :wait
21
+ current_parallel_group = nil
22
+ grouped_items << { type: :wait, step: step }
23
+ elsif step[:parallel_group].present?
24
+ pg = parallel_groups.find { |g| g[:name] == step[:parallel_group] }
25
+ if current_parallel_group != step[:parallel_group]
26
+ current_parallel_group = step[:parallel_group]
27
+ grouped_items << { type: :parallel_group, group: pg, steps: [step] }
28
+ else
29
+ grouped_items.last[:steps] << step
30
+ end
31
+ else
32
+ current_parallel_group = nil
33
+ grouped_items << { type: :step, step: step }
34
+ end
35
+ end
36
+ %>
37
+
38
+ <% grouped_items.each_with_index do |item, index| %>
39
+ <% if item[:type] == :wait %>
40
+ <!-- Wait Step Box -->
41
+ <% step = item[:step] %>
42
+ <%
43
+ wait_type = step[:wait_type]
44
+ case wait_type
45
+ when :delay
46
+ border_color = "border-teal-300 dark:border-teal-600"
47
+ bg_color = "bg-teal-50 dark:bg-teal-900/30"
48
+ text_color = "text-teal-700 dark:text-teal-300"
49
+ icon_svg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
50
+ type_label = "delay"
51
+ when :until
52
+ border_color = "border-cyan-300 dark:border-cyan-600"
53
+ bg_color = "bg-cyan-50 dark:bg-cyan-900/30"
54
+ text_color = "text-cyan-700 dark:text-cyan-300"
55
+ icon_svg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/></svg>'
56
+ type_label = "poll"
57
+ when :schedule
58
+ border_color = "border-indigo-300 dark:border-indigo-600"
59
+ bg_color = "bg-indigo-50 dark:bg-indigo-900/30"
60
+ text_color = "text-indigo-700 dark:text-indigo-300"
61
+ icon_svg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z"/></svg>'
62
+ type_label = "scheduled"
63
+ when :approval
64
+ border_color = "border-orange-300 dark:border-orange-600"
65
+ bg_color = "bg-orange-50 dark:bg-orange-900/30"
66
+ text_color = "text-orange-700 dark:text-orange-300"
67
+ icon_svg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
68
+ type_label = "approval"
69
+ else
70
+ border_color = "border-gray-300 dark:border-gray-600"
71
+ bg_color = "bg-gray-50 dark:bg-gray-900/30"
72
+ text_color = "text-gray-700 dark:text-gray-300"
73
+ icon_svg = '<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/></svg>'
74
+ type_label = "wait"
75
+ end
76
+ %>
77
+ <div class="flex flex-col items-center">
78
+ <div class="w-28 h-16 flex flex-col items-center justify-center rounded-lg border-2 border-dashed <%= border_color %> <%= bg_color %> relative">
79
+ <!-- Wait type icon -->
80
+ <div class="absolute -top-2 -right-2 <%= text_color %>">
81
+ <%= raw icon_svg %>
82
+ </div>
83
+ <span class="text-sm font-semibold <%= text_color %> truncate max-w-24 px-1">
84
+ <%= step[:ui_label] || step[:name].to_s.gsub(/_/, ' ').titleize %>
85
+ </span>
86
+ <% if step[:duration] %>
87
+ <span class="text-[10px] text-gray-500 dark:text-gray-400">
88
+ <%= step[:duration].is_a?(Numeric) ? "#{step[:duration]}s" : step[:duration] %>
89
+ </span>
90
+ <% elsif step[:timeout] %>
91
+ <span class="text-[10px] text-gray-500 dark:text-gray-400">
92
+ timeout: <%= step[:timeout].is_a?(Numeric) ? "#{step[:timeout]}s" : step[:timeout] %>
93
+ </span>
94
+ <% end %>
95
+ </div>
96
+ <span class="text-xs <%= text_color %> mt-1">
97
+ <%= type_label %>
98
+ </span>
99
+ </div>
100
+ <% elsif item[:type] == :step %>
101
+ <% step = item[:step] %>
102
+ <!-- Single Step Box -->
103
+ <div class="flex flex-col items-center">
104
+ <%
105
+ # Determine step styling based on type
106
+ if step[:routing]
107
+ border_color = "border-amber-300 dark:border-amber-600"
108
+ bg_color = "bg-amber-50 dark:bg-amber-900/30"
109
+ text_color = "text-amber-700 dark:text-amber-300"
110
+ icon_color = "text-amber-400 dark:text-amber-500"
111
+ else
112
+ border_color = "border-indigo-200 dark:border-indigo-700"
113
+ bg_color = "bg-indigo-50 dark:bg-indigo-900/30"
114
+ text_color = "text-indigo-700 dark:text-indigo-300"
115
+ icon_color = "text-indigo-400 dark:text-indigo-500"
116
+ end
117
+ %>
118
+ <div class="w-28 h-16 flex flex-col items-center justify-center rounded-lg border-2 <%= border_color %> <%= bg_color %> <%= step[:optional] ? 'border-dashed' : '' %> relative">
119
+ <% if step[:routing] %>
120
+ <!-- Routing indicator -->
121
+ <div class="absolute -top-2 -right-2">
122
+ <svg class="w-4 h-4 <%= icon_color %>" fill="currentColor" viewBox="0 0 20 20">
123
+ <path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
124
+ </svg>
125
+ </div>
126
+ <% end %>
127
+ <span class="text-sm font-semibold <%= text_color %> truncate max-w-24 px-1">
128
+ <%= step[:name] %>
129
+ </span>
130
+ <% if step[:ui_label].present? %>
131
+ <span class="text-[10px] text-gray-500 dark:text-gray-400 truncate max-w-24">
132
+ <%= step[:ui_label] %>
133
+ </span>
134
+ <% end %>
135
+ </div>
136
+ <span class="text-xs text-gray-500 dark:text-gray-400 mt-1 truncate max-w-28" title="<%= step[:agent] %>">
137
+ <%= step[:agent]&.to_s&.gsub(/Agent$/, '') %>
138
+ </span>
139
+ <% if step[:optional] %>
140
+ <span class="text-[10px] text-gray-400 dark:text-gray-500 italic">(optional)</span>
141
+ <% end %>
142
+ </div>
143
+ <% else %>
144
+ <!-- Parallel Group Box -->
145
+ <div class="flex flex-col items-center">
146
+ <div class="relative border-2 border-dashed border-purple-300 dark:border-purple-600 rounded-lg p-3 bg-purple-50/50 dark:bg-purple-900/20">
147
+ <!-- Fork indicator at top -->
148
+ <div class="absolute -top-3 left-1/2 transform -translate-x-1/2">
149
+ <div class="bg-purple-100 dark:bg-purple-800 rounded-full px-2 py-0.5">
150
+ <span class="text-[10px] font-medium text-purple-700 dark:text-purple-300">parallel</span>
151
+ </div>
152
+ </div>
153
+
154
+ <div class="flex items-center gap-2 mt-1">
155
+ <% item[:steps].each_with_index do |step, step_idx| %>
156
+ <div class="flex flex-col items-center">
157
+ <div class="w-24 h-14 flex flex-col items-center justify-center rounded-lg border border-purple-200 dark:border-purple-700 bg-white dark:bg-gray-800 <%= step[:optional] ? 'border-dashed' : '' %>">
158
+ <span class="text-xs font-semibold text-purple-700 dark:text-purple-300 truncate max-w-20 px-1">
159
+ <%= step[:name] %>
160
+ </span>
161
+ </div>
162
+ <span class="text-[10px] text-gray-500 dark:text-gray-400 mt-0.5 truncate max-w-24">
163
+ <%= step[:agent]&.to_s&.gsub(/Agent$/, '') %>
164
+ </span>
165
+ </div>
166
+ <% end %>
167
+ </div>
168
+
169
+ <% if item[:group] %>
170
+ <div class="flex items-center justify-center gap-2 mt-2 text-[10px] text-purple-600 dark:text-purple-400">
171
+ <% if item[:group][:fail_fast] %>
172
+ <span class="bg-purple-100 dark:bg-purple-900/50 px-1.5 py-0.5 rounded">fail_fast</span>
173
+ <% end %>
174
+ <% if item[:group][:concurrency] %>
175
+ <span class="bg-purple-100 dark:bg-purple-900/50 px-1.5 py-0.5 rounded">max: <%= item[:group][:concurrency] %></span>
176
+ <% end %>
177
+ </div>
178
+ <% end %>
179
+ </div>
180
+ </div>
181
+ <% end %>
182
+
183
+ <!-- Arrow between items -->
184
+ <% unless index == grouped_items.length - 1 %>
185
+ <% next_item = grouped_items[index + 1] %>
186
+ <% current_step = item[:type] == :step ? item[:step] : nil %>
187
+ <% if current_step&.dig(:routing) %>
188
+ <!-- Branching arrow for routing steps -->
189
+ <div class="flex items-center text-amber-400 dark:text-amber-500 mt-4">
190
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
191
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 5l7 7-7 7M5 5l7 7-7 7"/>
192
+ </svg>
193
+ </div>
194
+ <% else %>
195
+ <div class="flex items-center text-indigo-400 dark:text-indigo-500 mt-4">
196
+ <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
197
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7m0 0l-7 7m7-7H3"/>
198
+ </svg>
199
+ </div>
200
+ <% end %>
201
+ <% end %>
202
+ <% end %>
203
+ </div>
204
+ </div>
205
+
206
+ <!-- Input Schema (if defined) -->
207
+ <% if input_schema_fields.present? %>
208
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
209
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
210
+ Input Schema
211
+ </p>
212
+
213
+ <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-3">
214
+ <% input_schema_fields.each do |name, field| %>
215
+ <div class="flex items-start gap-2 py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-900/50">
216
+ <span class="w-2 h-2 rounded-full mt-1.5 <%= field[:required] ? 'bg-red-400' : 'bg-gray-300 dark:bg-gray-600' %>"></span>
217
+ <div class="flex-1 min-w-0">
218
+ <div class="flex items-center gap-1">
219
+ <span class="font-medium text-gray-700 dark:text-gray-300 text-sm truncate">
220
+ <%= name %>
221
+ </span>
222
+ <span class="text-[10px] text-gray-500 dark:text-gray-400">
223
+ <%= field[:type] %>
224
+ </span>
225
+ </div>
226
+ <% if field[:default].present? || field[:default] == false %>
227
+ <span class="text-[10px] text-gray-500 dark:text-gray-400">
228
+ default: <%= field[:default].inspect %>
229
+ </span>
230
+ <% end %>
231
+ </div>
232
+ </div>
233
+ <% end %>
234
+ </div>
235
+ </div>
236
+ <% end %>
237
+
238
+ <!-- Step Details Table -->
239
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
240
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
241
+ Step Details
242
+ </p>
243
+
244
+ <div class="space-y-2">
245
+ <% steps.each_with_index do |step, index| %>
246
+ <%
247
+ # Determine row styling
248
+ if step[:type] == :wait
249
+ case step[:wait_type]
250
+ when :delay
251
+ badge_bg = "bg-teal-100 dark:bg-teal-900/50"
252
+ badge_text = "text-teal-600 dark:text-teal-300"
253
+ when :until
254
+ badge_bg = "bg-cyan-100 dark:bg-cyan-900/50"
255
+ badge_text = "text-cyan-600 dark:text-cyan-300"
256
+ when :schedule
257
+ badge_bg = "bg-indigo-100 dark:bg-indigo-900/50"
258
+ badge_text = "text-indigo-600 dark:text-indigo-300"
259
+ when :approval
260
+ badge_bg = "bg-orange-100 dark:bg-orange-900/50"
261
+ badge_text = "text-orange-600 dark:text-orange-300"
262
+ else
263
+ badge_bg = "bg-gray-100 dark:bg-gray-900/50"
264
+ badge_text = "text-gray-600 dark:text-gray-300"
265
+ end
266
+ elsif step[:routing]
267
+ badge_bg = "bg-amber-100 dark:bg-amber-900/50"
268
+ badge_text = "text-amber-600 dark:text-amber-300"
269
+ elsif step[:parallel_group]
270
+ badge_bg = "bg-purple-100 dark:bg-purple-900/50"
271
+ badge_text = "text-purple-600 dark:text-purple-300"
272
+ else
273
+ badge_bg = "bg-indigo-100 dark:bg-indigo-900/50"
274
+ badge_text = "text-indigo-600 dark:text-indigo-300"
275
+ end
276
+ %>
277
+ <div class="flex items-start gap-3 py-2 px-3 rounded-lg bg-gray-50 dark:bg-gray-900/50">
278
+ <span class="w-6 h-6 flex items-center justify-center rounded-full <%= badge_bg %> text-xs font-medium <%= badge_text %>">
279
+ <%= index + 1 %>
280
+ </span>
281
+ <div class="flex-1 min-w-0">
282
+ <div class="flex items-center flex-wrap gap-2">
283
+ <span class="font-medium text-gray-700 dark:text-gray-300">
284
+ <%= step[:name] %>
285
+ </span>
286
+ <% if step[:type] == :wait %>
287
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
288
+ <span class="text-sm <%= badge_text %> italic">
289
+ (<%= step[:wait_type] %> wait)
290
+ </span>
291
+ <% elsif step[:agent] %>
292
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
293
+ <code class="text-sm font-mono text-gray-600 dark:text-gray-400 bg-gray-100 dark:bg-gray-800 px-2 py-0.5 rounded truncate max-w-xs">
294
+ <%= step[:agent] %>
295
+ </code>
296
+ <% else %>
297
+ <span class="text-gray-400 dark:text-gray-500 text-sm">-></span>
298
+ <span class="text-sm text-gray-500 dark:text-gray-400 italic">
299
+ (block)
300
+ </span>
301
+ <% end %>
302
+
303
+ <!-- Badges -->
304
+ <% if step[:routing] %>
305
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300">
306
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
307
+ <path fill-rule="evenodd" d="M7.707 3.293a1 1 0 010 1.414L5.414 7H11a7 7 0 017 7v2a1 1 0 11-2 0v-2a5 5 0 00-5-5H5.414l2.293 2.293a1 1 0 11-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd"/>
308
+ </svg>
309
+ routing
310
+ </span>
311
+ <% end %>
312
+
313
+ <% if step[:parallel_group] %>
314
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300">
315
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
316
+ <path d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z"/>
317
+ </svg>
318
+ <%= step[:parallel_group] %>
319
+ </span>
320
+ <% end %>
321
+
322
+ <% if step[:optional] %>
323
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">
324
+ optional
325
+ </span>
326
+ <% end %>
327
+
328
+ <% if step[:timeout] %>
329
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">
330
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
331
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm1-12a1 1 0 10-2 0v4a1 1 0 00.293.707l2.828 2.829a1 1 0 101.415-1.415L11 9.586V6z" clip-rule="evenodd"/>
332
+ </svg>
333
+ <%= step[:timeout].is_a?(Numeric) ? "#{step[:timeout]}s" : step[:timeout] %>
334
+ </span>
335
+ <% end %>
336
+
337
+ <% if step[:type] == :wait %>
338
+ <% if step[:duration] %>
339
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-teal-100 dark:bg-teal-900/50 text-teal-700 dark:text-teal-300">
340
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
341
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
342
+ </svg>
343
+ <%= step[:duration].is_a?(Numeric) ? "#{step[:duration]}s" : step[:duration] %>
344
+ </span>
345
+ <% end %>
346
+
347
+ <% if step[:poll_interval] %>
348
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300">
349
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
350
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
351
+ </svg>
352
+ poll: <%= step[:poll_interval].is_a?(Numeric) ? "#{step[:poll_interval]}s" : step[:poll_interval] %>
353
+ </span>
354
+ <% end %>
355
+
356
+ <% if step[:on_timeout] %>
357
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 dark:bg-yellow-900/50 text-yellow-700 dark:text-yellow-300">
358
+ on_timeout: <%= step[:on_timeout] %>
359
+ </span>
360
+ <% end %>
361
+
362
+ <% if step[:notify].present? %>
363
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300">
364
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
365
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 17h5l-1.405-1.405A2.032 2.032 0 0118 14.158V11a6.002 6.002 0 00-4-5.659V5a2 2 0 10-4 0v.341C7.67 6.165 6 8.388 6 11v3.159c0 .538-.214 1.055-.595 1.436L4 17h5m6 0v1a3 3 0 11-6 0v-1m6 0H9"/>
366
+ </svg>
367
+ notify: <%= Array(step[:notify]).join(", ") %>
368
+ </span>
369
+ <% end %>
370
+
371
+ <% if step[:approvers].present? %>
372
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300">
373
+ <svg class="w-3 h-3 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
374
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4.354a4 4 0 110 5.292M15 21H3v-1a6 6 0 0112 0v1zm0 0h6v-1a6 6 0 00-9-5.197M13 7a4 4 0 11-8 0 4 4 0 018 0z"/>
375
+ </svg>
376
+ <%= Array(step[:approvers]).size %> approver(s)
377
+ </span>
378
+ <% end %>
379
+ <% end %>
380
+ </div>
381
+
382
+ <% if step[:description].present? %>
383
+ <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
384
+ <%= step[:description] %>
385
+ </p>
386
+ <% end %>
387
+ </div>
388
+ </div>
389
+ <% end %>
390
+ </div>
391
+ </div>
392
+
393
+ <!-- Parallel Groups Summary -->
394
+ <% if parallel_groups.present? %>
395
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
396
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
397
+ Parallel Groups
398
+ </p>
399
+
400
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
401
+ <% parallel_groups.each do |group| %>
402
+ <div class="flex items-start gap-3 py-2 px-3 rounded-lg bg-purple-50 dark:bg-purple-900/20 border border-purple-200 dark:border-purple-800">
403
+ <span class="w-6 h-6 flex items-center justify-center rounded-full bg-purple-100 dark:bg-purple-900/50 text-xs font-medium text-purple-600 dark:text-purple-300">
404
+ <svg class="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
405
+ <path d="M10 3.5a1.5 1.5 0 013 0V4a1 1 0 001 1h3a1 1 0 011 1v3a1 1 0 01-1 1h-.5a1.5 1.5 0 000 3h.5a1 1 0 011 1v3a1 1 0 01-1 1h-3a1 1 0 01-1-1v-.5a1.5 1.5 0 00-3 0v.5a1 1 0 01-1 1H6a1 1 0 01-1-1v-3a1 1 0 00-1-1h-.5a1.5 1.5 0 010-3H4a1 1 0 001-1V6a1 1 0 011-1h3a1 1 0 001-1v-.5z"/>
406
+ </svg>
407
+ </span>
408
+ <div class="flex-1">
409
+ <div class="flex items-center gap-2">
410
+ <span class="font-medium text-purple-700 dark:text-purple-300">
411
+ <%= group[:name] %>
412
+ </span>
413
+ <span class="text-sm text-gray-500 dark:text-gray-400">
414
+ (<%= group[:step_names]&.size || 0 %> steps)
415
+ </span>
416
+ </div>
417
+ <div class="flex items-center gap-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
418
+ <% if group[:fail_fast] %>
419
+ <span>fail_fast: on</span>
420
+ <% end %>
421
+ <% if group[:concurrency] %>
422
+ <span>concurrency: <%= group[:concurrency] %></span>
423
+ <% end %>
424
+ <% if group[:timeout] %>
425
+ <span>timeout: <%= group[:timeout] %>s</span>
426
+ <% end %>
427
+ </div>
428
+ <div class="flex flex-wrap gap-1 mt-1">
429
+ <% group[:step_names]&.each do |step_name| %>
430
+ <span class="text-xs bg-purple-100 dark:bg-purple-900/50 text-purple-600 dark:text-purple-300 px-1.5 py-0.5 rounded">
431
+ <%= step_name %>
432
+ </span>
433
+ <% end %>
434
+ </div>
435
+ </div>
436
+ </div>
437
+ <% end %>
438
+ </div>
439
+ </div>
440
+ <% end %>
441
+
442
+ <!-- Wait Steps Summary -->
443
+ <% wait_steps = steps.select { |s| s[:type] == :wait } %>
444
+ <% if wait_steps.present? %>
445
+ <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mt-4">
446
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
447
+ Wait Steps
448
+ </p>
449
+
450
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-3">
451
+ <% wait_steps.each do |wait_step| %>
452
+ <%
453
+ case wait_step[:wait_type]
454
+ when :delay
455
+ bg_color = "bg-teal-50 dark:bg-teal-900/20"
456
+ border_color = "border-teal-200 dark:border-teal-800"
457
+ icon_color = "text-teal-600 dark:text-teal-300"
458
+ icon_bg = "bg-teal-100 dark:bg-teal-900/50"
459
+ type_label = "Delay"
460
+ when :until
461
+ bg_color = "bg-cyan-50 dark:bg-cyan-900/20"
462
+ border_color = "border-cyan-200 dark:border-cyan-800"
463
+ icon_color = "text-cyan-600 dark:text-cyan-300"
464
+ icon_bg = "bg-cyan-100 dark:bg-cyan-900/50"
465
+ type_label = "Poll Until"
466
+ when :schedule
467
+ bg_color = "bg-indigo-50 dark:bg-indigo-900/20"
468
+ border_color = "border-indigo-200 dark:border-indigo-800"
469
+ icon_color = "text-indigo-600 dark:text-indigo-300"
470
+ icon_bg = "bg-indigo-100 dark:bg-indigo-900/50"
471
+ type_label = "Scheduled"
472
+ when :approval
473
+ bg_color = "bg-orange-50 dark:bg-orange-900/20"
474
+ border_color = "border-orange-200 dark:border-orange-800"
475
+ icon_color = "text-orange-600 dark:text-orange-300"
476
+ icon_bg = "bg-orange-100 dark:bg-orange-900/50"
477
+ type_label = "Approval"
478
+ else
479
+ bg_color = "bg-gray-50 dark:bg-gray-900/20"
480
+ border_color = "border-gray-200 dark:border-gray-800"
481
+ icon_color = "text-gray-600 dark:text-gray-300"
482
+ icon_bg = "bg-gray-100 dark:bg-gray-900/50"
483
+ type_label = "Wait"
484
+ end
485
+ %>
486
+ <div class="flex items-start gap-3 py-2 px-3 rounded-lg <%= bg_color %> border <%= border_color %>">
487
+ <span class="w-6 h-6 flex items-center justify-center rounded-full <%= icon_bg %> text-xs font-medium <%= icon_color %>">
488
+ <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
489
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z"/>
490
+ </svg>
491
+ </span>
492
+ <div class="flex-1">
493
+ <div class="flex items-center gap-2">
494
+ <span class="font-medium <%= icon_color %>">
495
+ <%= wait_step[:ui_label] || wait_step[:name] %>
496
+ </span>
497
+ <span class="text-xs px-1.5 py-0.5 rounded <%= icon_bg %> <%= icon_color %>">
498
+ <%= type_label %>
499
+ </span>
500
+ </div>
501
+ <div class="flex flex-wrap items-center gap-2 mt-1 text-xs text-gray-500 dark:text-gray-400">
502
+ <% if wait_step[:duration] %>
503
+ <span>duration: <%= wait_step[:duration].is_a?(Numeric) ? "#{wait_step[:duration]}s" : wait_step[:duration] %></span>
504
+ <% end %>
505
+ <% if wait_step[:poll_interval] %>
506
+ <span>poll: <%= wait_step[:poll_interval].is_a?(Numeric) ? "#{wait_step[:poll_interval]}s" : wait_step[:poll_interval] %></span>
507
+ <% end %>
508
+ <% if wait_step[:timeout] %>
509
+ <span>timeout: <%= wait_step[:timeout].is_a?(Numeric) ? "#{wait_step[:timeout]}s" : wait_step[:timeout] %></span>
510
+ <% end %>
511
+ <% if wait_step[:on_timeout] %>
512
+ <span>on_timeout: <%= wait_step[:on_timeout] %></span>
513
+ <% end %>
514
+ </div>
515
+ <% if wait_step[:notify].present? || wait_step[:approvers].present? %>
516
+ <div class="flex flex-wrap gap-1 mt-1">
517
+ <% if wait_step[:notify].present? %>
518
+ <span class="text-xs bg-blue-100 dark:bg-blue-900/50 text-blue-600 dark:text-blue-300 px-1.5 py-0.5 rounded">
519
+ notify: <%= Array(wait_step[:notify]).join(", ") %>
520
+ </span>
521
+ <% end %>
522
+ <% if wait_step[:approvers].present? %>
523
+ <span class="text-xs bg-orange-100 dark:bg-orange-900/50 text-orange-600 dark:text-orange-300 px-1.5 py-0.5 rounded">
524
+ <%= Array(wait_step[:approvers]).size %> approver(s)
525
+ </span>
526
+ <% end %>
527
+ </div>
528
+ <% end %>
529
+ </div>
530
+ </div>
531
+ <% end %>
532
+ </div>
533
+ </div>
534
+ <% end %>
535
+ <% else %>
536
+ <p class="text-gray-500 dark:text-gray-400 italic py-4">
537
+ No steps defined for this DSL workflow.
538
+ </p>
539
+ <% end %>