ruby_llm-agents 1.0.0.beta.1 → 1.1.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 (139) 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/services/ruby_llm/agents/agent_registry.rb +18 -12
  16. data/app/views/layouts/ruby_llm/agents/application.html.erb +72 -76
  17. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -12
  18. data/app/views/ruby_llm/agents/agents/_sortable_header.html.erb +56 -0
  19. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +5 -15
  20. data/app/views/ruby_llm/agents/agents/index.html.erb +271 -100
  21. data/app/views/ruby_llm/agents/agents/show.html.erb +1 -0
  22. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +107 -0
  23. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +18 -0
  24. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +4 -1
  25. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +66 -359
  26. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +56 -0
  27. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +115 -0
  28. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +35 -60
  29. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +17 -6
  30. data/app/views/ruby_llm/agents/dashboard/index.html.erb +373 -72
  31. data/app/views/ruby_llm/agents/executions/_execution.html.erb +0 -1
  32. data/app/views/ruby_llm/agents/executions/_filters.html.erb +51 -39
  33. data/app/views/ruby_llm/agents/executions/_list.html.erb +53 -195
  34. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +5 -20
  35. data/app/views/ruby_llm/agents/executions/index.html.erb +7 -83
  36. data/app/views/ruby_llm/agents/executions/show.html.erb +10 -20
  37. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +2 -1
  38. data/app/views/ruby_llm/agents/shared/_doc_link.html.erb +12 -0
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +3 -15
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +1 -1
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +1 -1
  42. data/app/views/ruby_llm/agents/shared/_sortable_header.html.erb +53 -0
  43. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +7 -0
  44. data/app/views/ruby_llm/agents/shared/_status_dot.html.erb +1 -1
  45. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +9 -35
  46. data/app/views/ruby_llm/agents/system_config/show.html.erb +4 -1
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +4 -1
  48. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +7 -15
  49. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +539 -0
  50. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +920 -0
  51. data/app/views/ruby_llm/agents/workflows/index.html.erb +179 -0
  52. data/app/views/ruby_llm/agents/workflows/show.html.erb +164 -139
  53. data/config/routes.rb +1 -1
  54. data/lib/generators/ruby_llm_agents/agent_generator.rb +6 -36
  55. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +7 -37
  56. data/lib/generators/ruby_llm_agents/embedder_generator.rb +5 -38
  57. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +7 -37
  58. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +7 -37
  59. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +8 -41
  60. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +18 -46
  61. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +7 -37
  62. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +7 -37
  63. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +7 -37
  64. data/lib/generators/ruby_llm_agents/install_generator.rb +33 -56
  65. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +480 -0
  66. data/lib/generators/ruby_llm_agents/restructure_generator.rb +2 -2
  67. data/lib/generators/ruby_llm_agents/speaker_generator.rb +8 -39
  68. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +5 -8
  69. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +40 -42
  70. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +20 -22
  71. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +24 -26
  72. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +20 -22
  73. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +19 -17
  74. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +31 -33
  75. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +125 -127
  76. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +20 -18
  77. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +19 -17
  78. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +19 -17
  79. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +38 -40
  80. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +42 -44
  81. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +48 -0
  82. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +19 -21
  83. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +19 -21
  84. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +20 -22
  85. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +15 -17
  86. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +25 -27
  87. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +19 -21
  88. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +20 -22
  89. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +17 -19
  90. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +15 -17
  91. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +87 -24
  92. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +21 -27
  93. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +46 -54
  94. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +31 -39
  95. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +22 -28
  96. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +53 -63
  97. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +46 -56
  98. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +23 -31
  99. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +22 -30
  100. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +23 -31
  101. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +38 -46
  102. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +7 -7
  103. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +59 -71
  104. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +274 -23
  105. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +29 -31
  106. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +28 -30
  107. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +10 -43
  108. data/lib/ruby_llm/agents/core/configuration.rb +55 -43
  109. data/lib/ruby_llm/agents/core/version.rb +1 -1
  110. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +26 -0
  111. data/lib/ruby_llm/agents/pipeline.rb +69 -0
  112. data/lib/ruby_llm/agents/workflow/approval.rb +205 -0
  113. data/lib/ruby_llm/agents/workflow/approval_store.rb +179 -0
  114. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +467 -0
  115. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +244 -0
  116. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +289 -0
  117. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +107 -0
  118. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +150 -0
  119. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +187 -0
  120. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +352 -0
  121. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +415 -0
  122. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +257 -0
  123. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +317 -0
  124. data/lib/ruby_llm/agents/workflow/dsl.rb +576 -0
  125. data/lib/ruby_llm/agents/workflow/instrumentation.rb +2 -7
  126. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +117 -0
  127. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +117 -0
  128. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +180 -0
  129. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +121 -0
  130. data/lib/ruby_llm/agents/workflow/notifiers.rb +70 -0
  131. data/lib/ruby_llm/agents/workflow/orchestrator.rb +190 -23
  132. data/lib/ruby_llm/agents/workflow/result.rb +202 -0
  133. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +206 -0
  134. data/lib/ruby_llm/agents/workflow/wait_result.rb +213 -0
  135. metadata +37 -6
  136. data/app/views/ruby_llm/agents/dashboard/_execution_item.html.erb +0 -66
  137. data/lib/ruby_llm/agents/workflow/parallel.rb +0 -299
  138. data/lib/ruby_llm/agents/workflow/pipeline.rb +0 -306
  139. data/lib/ruby_llm/agents/workflow/router.rb +0 -429
@@ -3,17 +3,25 @@
3
3
  selected_agents = params[:agent_types].present? ? (params[:agent_types].is_a?(Array) ? params[:agent_types] : params[:agent_types].split(",")) : []
4
4
  selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
5
5
  selected_models = params[:model_ids].present? ? (params[:model_ids].is_a?(Array) ? params[:model_ids] : params[:model_ids].split(",")) : []
6
- selected_workflows = params[:workflow_types].present? ? (params[:workflow_types].is_a?(Array) ? params[:workflow_types] : params[:workflow_types].split(",")) : []
6
+ selected_execution_type = params[:execution_type].presence
7
7
 
8
- has_filters = selected_agents.any? || selected_statuses.any? || params[:days].present? || selected_models.any? || selected_workflows.any?
8
+ has_filters = selected_agents.any? || selected_statuses.any? || params[:days].present? || selected_models.any? || params[:tenant_id].present? || selected_execution_type.present?
9
9
  active_filter_count = [
10
10
  selected_agents.any? ? 1 : 0,
11
11
  selected_statuses.any? ? 1 : 0,
12
12
  params[:days].present? ? 1 : 0,
13
13
  selected_models.any? ? 1 : 0,
14
- selected_workflows.any? ? 1 : 0
14
+ params[:tenant_id].present? ? 1 : 0,
15
+ selected_execution_type.present? ? 1 : 0
15
16
  ].sum
16
17
 
18
+ # Execution type options (replaces the old tabs)
19
+ execution_type_options = [
20
+ { value: "", label: "All Executions" },
21
+ { value: "agents", label: "Agents Only" },
22
+ { value: "workflows", label: "Workflows" }
23
+ ]
24
+
17
25
  status_options = [
18
26
  { value: "success", label: "Success", color: "bg-green-500" },
19
27
  { value: "error", label: "Error", color: "bg-red-500" },
@@ -35,20 +43,8 @@
35
43
  short_name = m.split("/").last.split(":").first # Handle "provider/model:version" format
36
44
  { value: m, label: short_name }
37
45
  end || []
38
-
39
- # Build workflow type options with icons
40
- workflow_type_labels = {
41
- "pipeline" => { label: "Pipeline", icon: "→" },
42
- "parallel" => { label: "Parallel", icon: "⫴" },
43
- "router" => { label: "Router", icon: "⑂" }
44
- }
45
- workflow_options = [{ value: "single", label: "Single Agent" }]
46
- workflow_options += (local_assigns[:workflow_types] || []).map do |wt|
47
- info = workflow_type_labels[wt] || { label: wt.titleize, icon: "" }
48
- { value: wt, label: "#{info[:icon]} #{info[:label]}".strip }
49
- end
50
46
  %>
51
- <div id="filters-container" class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-4 mb-6"
47
+ <div id="filters-container" class="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-3 mb-6"
52
48
  x-data="{ mobileOpen: false }">
53
49
  <%= form_with url: ruby_llm_agents.executions_path, method: :get,
54
50
  local: true,
@@ -82,7 +78,19 @@
82
78
  :class="{ 'hidden md:block': !mobileOpen, 'block': mobileOpen }">
83
79
 
84
80
  <%# Responsive flex: column on mobile, row on desktop %>
85
- <div class="flex flex-col md:flex-row md:items-center gap-3">
81
+ <div class="flex flex-col md:flex-row md:items-center gap-2">
82
+ <%# Execution Type Filter (replaces tabs) %>
83
+ <div class="md:w-auto">
84
+ <%= render "ruby_llm/agents/shared/select_dropdown",
85
+ name: "execution_type",
86
+ filter_id: "execution_type",
87
+ options: execution_type_options,
88
+ selected: selected_execution_type,
89
+ icon: "M4 6h16M4 10h16M4 14h16M4 18h16",
90
+ width: "w-44",
91
+ full_width: true %>
92
+ </div>
93
+
86
94
  <%# Status Filter %>
87
95
  <div class="md:w-auto">
88
96
  <%= render "ruby_llm/agents/shared/filter_dropdown",
@@ -96,6 +104,23 @@
96
104
  full_width: true %>
97
105
  </div>
98
106
 
107
+ <%# Visual separator between filter groups %>
108
+ <div class="hidden md:block w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
109
+
110
+ <%# Tenant Filter %>
111
+ <% if tenant_filter_enabled? && available_tenants.any? %>
112
+ <div class="md:w-auto">
113
+ <%= render "ruby_llm/agents/shared/select_dropdown",
114
+ name: "tenant_id",
115
+ filter_id: "tenant_id",
116
+ options: [{ value: "", label: "All Tenants" }] + available_tenants.map { |t| { value: t, label: t.truncate(15) } },
117
+ selected: params[:tenant_id],
118
+ icon: "M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4",
119
+ width: "w-40",
120
+ full_width: true %>
121
+ </div>
122
+ <% end %>
123
+
99
124
  <%# Time Range Filter %>
100
125
  <div class="md:w-auto">
101
126
  <%= render "ruby_llm/agents/shared/select_dropdown",
@@ -108,6 +133,9 @@
108
133
  full_width: true %>
109
134
  </div>
110
135
 
136
+ <%# Visual separator between filter groups %>
137
+ <div class="hidden md:block w-px h-6 bg-gray-200 dark:bg-gray-700 mx-1"></div>
138
+
111
139
  <%# Model Filter %>
112
140
  <% if model_options.any? %>
113
141
  <div class="md:w-auto">
@@ -139,33 +167,17 @@
139
167
  </div>
140
168
  <% end %>
141
169
 
142
- <%# Workflow Type Filter %>
143
- <% if workflow_options.length > 1 %>
144
- <div class="md:w-auto">
145
- <%= render "ruby_llm/agents/shared/filter_dropdown",
146
- name: "workflow_types[]",
147
- filter_id: "workflow_types",
148
- label: "Type",
149
- all_label: "All Types",
150
- options: workflow_options,
151
- selected: selected_workflows,
152
- icon: "M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm0 8a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zm12 0a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z",
153
- width: "w-44",
154
- full_width: true %>
155
- </div>
156
- <% end %>
157
-
158
170
  <%# Spacer (desktop only) %>
159
171
  <div class="hidden md:block flex-1"></div>
160
172
 
161
173
  <%# Stats %>
162
- <div id="filter-stats" class="flex items-center justify-between md:justify-end gap-3 py-2 px-3 md:p-0 bg-gray-50 md:bg-transparent dark:bg-gray-700/50 md:dark:bg-transparent rounded-lg md:rounded-none">
163
- <span class="text-xs text-gray-500 dark:text-gray-400 md:hidden">Results</span>
164
- <div class="flex items-center gap-3 text-xs text-gray-500 dark:text-gray-400">
174
+ <div id="filter-stats" class="flex items-center justify-between md:justify-end gap-2 py-2 px-3 md:p-0 bg-gray-50 md:bg-transparent dark:bg-gray-700/50 md:dark:bg-transparent rounded-lg md:rounded-none">
175
+ <span class="text-xs text-gray-400 dark:text-gray-500 md:hidden">Results</span>
176
+ <div class="flex items-center gap-2 text-xs text-gray-400 dark:text-gray-500">
165
177
  <span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_count]) %><span class="md:hidden"> executions</span></span>
166
- <span class="text-gray-300 dark:text-gray-600">|</span>
178
+ <span class="text-gray-300 dark:text-gray-600">/</span>
167
179
  <span class="tabular-nums"><%= number_to_human_short(filter_stats[:total_cost], prefix: "$", precision: 2) %></span>
168
- <span class="hidden md:inline text-gray-300 dark:text-gray-600">|</span>
180
+ <span class="hidden md:inline text-gray-300 dark:text-gray-600">/</span>
169
181
  <span class="hidden md:inline tabular-nums"><%= number_to_human_short(filter_stats[:total_tokens]) %>t</span>
170
182
  </div>
171
183
  </div>
@@ -182,7 +194,7 @@
182
194
  <span class="md:hidden">Clear</span>
183
195
  <% end %>
184
196
  <% end %>
185
- <%= link_to ruby_llm_agents.export_executions_path(agent_types: selected_agents.presence, statuses: selected_statuses.presence, days: params[:days].presence, model_ids: selected_models.presence, workflow_types: selected_workflows.presence),
197
+ <%= link_to ruby_llm_agents.export_executions_path(agent_types: selected_agents.presence, statuses: selected_statuses.presence, days: params[:days].presence, model_ids: selected_models.presence, tenant_id: params[:tenant_id].presence, execution_type: selected_execution_type),
186
198
  class: "flex-1 md:flex-initial flex items-center justify-center gap-2 px-3 py-2 md:p-2 text-sm md:text-base text-gray-600 md:text-gray-400 dark:text-gray-300 md:dark:text-gray-400 bg-gray-50 md:bg-transparent dark:bg-gray-700 md:dark:bg-transparent hover:text-gray-600 md:hover:text-gray-600 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-600 md:dark:hover:bg-gray-700 rounded-lg transition-colors",
187
199
  title: "Export CSV" do %>
188
200
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -9,115 +9,50 @@
9
9
  <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
10
10
  <thead class="bg-gray-50 dark:bg-gray-900">
11
11
  <tr>
12
- <th scope="col" class="px-2 py-3 w-8"></th>
13
- <th
14
- scope="col"
15
- class="
16
- px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400
17
- uppercase tracking-wider
18
- "
19
- >
20
- Agent
21
- </th>
12
+ <%= render "ruby_llm/agents/shared/sortable_header",
13
+ column: "agent_type", label: "Agent",
14
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
22
15
 
23
- <th
24
- scope="col"
25
- class="
26
- px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400
27
- uppercase tracking-wider
28
- "
29
- >
30
- Status
31
- </th>
16
+ <%= render "ruby_llm/agents/shared/sortable_header",
17
+ column: "status", label: "Status",
18
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
32
19
 
33
- <th
34
- scope="col"
35
- class="
36
- px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400
37
- uppercase tracking-wider
38
- "
39
- >
40
- Version
41
- </th>
20
+ <%= render "ruby_llm/agents/shared/sortable_header",
21
+ column: "model_id", label: "Model",
22
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
42
23
 
43
- <th
44
- scope="col"
45
- class="
46
- px-4 py-3 text-center text-xs font-medium text-gray-500 dark:text-gray-400
47
- uppercase tracking-wider
48
- "
49
- >
50
- Attempts
51
- </th>
24
+ <%= render "ruby_llm/agents/shared/sortable_header",
25
+ column: "agent_version", label: "Version",
26
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
52
27
 
53
- <th
54
- scope="col"
55
- class="
56
- px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400
57
- uppercase tracking-wider
58
- "
59
- >
60
- Tokens
61
- </th>
28
+ <%= render "ruby_llm/agents/shared/sortable_header",
29
+ column: "total_tokens", label: "Tokens",
30
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
62
31
 
63
- <th
64
- scope="col"
65
- class="
66
- px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400
67
- uppercase tracking-wider
68
- "
69
- >
70
- Cost
71
- </th>
32
+ <%= render "ruby_llm/agents/shared/sortable_header",
33
+ column: "total_cost", label: "Cost",
34
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
72
35
 
73
- <th
74
- scope="col"
75
- class="
76
- px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400
77
- uppercase tracking-wider
78
- "
79
- >
80
- Duration
81
- </th>
36
+ <%= render "ruby_llm/agents/shared/sortable_header",
37
+ column: "duration_ms", label: "Duration",
38
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
82
39
 
83
- <th
84
- scope="col"
85
- class="
86
- px-4 py-3 text-right text-xs font-medium text-gray-500 dark:text-gray-400
87
- uppercase tracking-wider
88
- "
89
- >
90
- Time
91
- </th>
40
+ <%= render "ruby_llm/agents/shared/sortable_header",
41
+ column: "created_at", label: "Time",
42
+ current_sort: @sort_params[:column], current_direction: @sort_params[:direction] %>
92
43
  </tr>
93
44
  </thead>
94
45
 
95
- <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100 dark:divide-gray-700">
96
- <% executions.each do |execution| %>
97
- <% has_attempts = execution.respond_to?(:attempts) && execution.attempts.present? && execution.attempts.size > 0 %>
46
+ <tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-100/50 dark:divide-gray-700/50">
47
+ <% executions.each_with_index do |execution, index| %>
48
+ <% attempts_count = (execution.respond_to?(:attempts_count) && execution.attempts_count) || (execution.respond_to?(:attempts) && execution.attempts&.size) || 1 %>
49
+ <% row_bg = index.even? ? '' : 'bg-gray-50/50 dark:bg-gray-900/30' %>
98
50
  <tr
99
- class="hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
51
+ class="<%= row_bg %> hover:bg-blue-50/50 dark:hover:bg-blue-900/20 transition-colors cursor-pointer"
100
52
  id="execution-row-<%= execution.id %>"
53
+ onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
101
54
  >
102
- <!-- Expand/Collapse Button -->
103
- <td class="px-2 py-3 whitespace-nowrap">
104
- <% if has_attempts %>
105
- <button
106
- type="button"
107
- onclick="toggleAttempts(<%= execution.id %>); event.stopPropagation();"
108
- class="expand-btn p-1 rounded hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors"
109
- data-execution-id="<%= execution.id %>"
110
- aria-expanded="false"
111
- aria-label="Toggle attempts"
112
- >
113
- <svg class="w-4 h-4 text-gray-500 dark:text-gray-400 transform transition-transform" fill="none" stroke="currentColor" viewBox="0 0 24 24">
114
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5l7 7-7 7"/>
115
- </svg>
116
- </button>
117
- <% end %>
118
- </td>
119
-
120
- <td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
55
+ <td class="px-4 py-3 whitespace-nowrap">
121
56
  <div class="flex items-center gap-2">
122
57
  <% if execution.respond_to?(:workflow_type) && execution.workflow_type.present? %>
123
58
  <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: execution.workflow_type, size: :xs, show_label: false %>
@@ -133,120 +68,43 @@
133
68
  <% end %>
134
69
  </td>
135
70
 
136
- <td class="px-4 py-3 whitespace-nowrap cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
137
- <%= render "ruby_llm/agents/shared/status_badge", status: execution.status %>
71
+ <td class="px-4 py-3 whitespace-nowrap">
72
+ <div>
73
+ <%= render "ruby_llm/agents/shared/status_badge", status: execution.status, attempts_count: attempts_count %>
74
+ </div>
75
+ <% if execution.status_error? && execution.error_class.present? %>
76
+ <span class="text-xs text-red-500 dark:text-red-400 block mt-0.5">
77
+ <%= execution.error_class.split("::").last %>
78
+ </span>
79
+ <% end %>
138
80
  </td>
139
81
 
140
- <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
141
- v<%= execution.agent_version %>
82
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
83
+ <span title="<%= execution.model_id %>">
84
+ <%= execution.model_id&.split("/")&.last || execution.model_id || "-" %>
85
+ </span>
142
86
  </td>
143
87
 
144
- <!-- Attempts Count -->
145
- <td class="px-4 py-3 whitespace-nowrap text-sm text-center cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
146
- <% attempts_count = (execution.respond_to?(:attempts_count) && execution.attempts_count) || (execution.respond_to?(:attempts) && execution.attempts&.size) || 1 %>
147
- <% if attempts_count > 1 %>
148
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200">
149
- <%= attempts_count %> attempts
150
- </span>
151
- <% else %>
152
- <span class="text-gray-400 dark:text-gray-500">1</span>
153
- <% end %>
88
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
89
+ v<%= execution.agent_version %>
154
90
  </td>
155
91
 
156
- <td
157
- class="
158
- px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
159
- text-right font-medium cursor-pointer
160
- "
161
- onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
162
- >
92
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
163
93
  <%= number_to_human_short(execution.total_tokens || 0) %>
164
94
  </td>
165
95
 
166
- <td
167
- class="
168
- px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
169
- text-right font-medium cursor-pointer
170
- "
171
- onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
172
- >
96
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
173
97
  <%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
174
98
  </td>
175
99
 
176
- <td
177
- class="
178
- px-4 py-3 whitespace-nowrap text-sm text-gray-900 dark:text-gray-100
179
- text-right font-medium cursor-pointer
180
- "
181
- onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
182
- >
100
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-700 dark:text-gray-300 font-mono">
183
101
  <%= number_with_delimiter(execution.duration_ms || 0) %>ms
184
102
  </td>
185
103
 
186
- <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400 text-right cursor-pointer" onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
104
+ <td class="px-4 py-3 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
187
105
  <%= time_ago_in_words(execution.created_at) %> ago
188
106
  </td>
189
107
  </tr>
190
-
191
- <!-- Inline Attempts Expansion Row (hidden by default) -->
192
- <% if has_attempts %>
193
- <tr id="attempts-row-<%= execution.id %>" class="hidden bg-gray-50 dark:bg-gray-900">
194
- <td colspan="9" class="px-4 py-3">
195
- <div class="ml-6">
196
- <h4 class="text-xs font-medium text-gray-500 dark:text-gray-400 uppercase mb-2">Attempts Timeline</h4>
197
- <div class="space-y-2">
198
- <% execution.attempts.each_with_index do |attempt, idx| %>
199
- <% is_successful = attempt["error_class"].nil? && !attempt["short_circuited"] %>
200
- <% is_short_circuited = attempt["short_circuited"] %>
201
- <div class="flex items-center gap-3 text-sm <%= is_successful ? 'bg-green-50 dark:bg-green-900/20 border-l-2 border-green-500' : is_short_circuited ? 'bg-gray-100 dark:bg-gray-800 border-l-2 border-gray-400' : 'bg-red-50 dark:bg-red-900/20 border-l-2 border-red-500' %> px-3 py-2 rounded-r">
202
- <span class="font-medium text-gray-700 dark:text-gray-300 w-6">#<%= idx + 1 %></span>
203
- <span class="text-gray-600 dark:text-gray-400 w-32 truncate" title="<%= attempt["model_id"] %>">
204
- <%= attempt["model_id"]&.split("/")&.last || "unknown" %>
205
- </span>
206
- <% if is_successful %>
207
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-800 dark:bg-green-800 dark:text-green-200">
208
- Success
209
- </span>
210
- <% elsif is_short_circuited %>
211
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-200 text-gray-600 dark:bg-gray-700 dark:text-gray-400">
212
- Skipped (breaker)
213
- </span>
214
- <% else %>
215
- <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 text-red-800 dark:bg-red-800 dark:text-red-200" title="<%= attempt["error_message"] %>">
216
- <%= attempt["error_class"]&.split("::")&.last || "Error" %>
217
- </span>
218
- <% end %>
219
- <% if attempt["duration_ms"] %>
220
- <span class="text-gray-500 dark:text-gray-400 text-xs">
221
- <%= number_with_delimiter(attempt["duration_ms"].to_i) %>ms
222
- </span>
223
- <% end %>
224
- <% if attempt["input_tokens"] || attempt["output_tokens"] %>
225
- <span class="text-gray-500 dark:text-gray-400 text-xs">
226
- <%= number_to_human_short((attempt["input_tokens"] || 0) + (attempt["output_tokens"] || 0)) %> tokens
227
- </span>
228
- <% end %>
229
- </div>
230
- <% end %>
231
- </div>
232
- </div>
233
- </td>
234
- </tr>
235
- <% end %>
236
-
237
- <% if execution.status_error? && execution.error_message.present? %>
238
- <tr class="bg-red-50 dark:bg-red-900/30">
239
- <td colspan="9" class="px-4 py-2">
240
- <p class="text-xs text-red-600 dark:text-red-400 ml-8">
241
- <span class="font-medium">
242
- <%= execution.error_class %>:
243
- </span>
244
-
245
- <%= truncate(execution.error_message, length: 150) %>
246
- </p>
247
- </td>
248
- </tr>
249
- <% end %>
250
108
  <% end %>
251
109
  </tbody>
252
110
  </table>
@@ -267,7 +125,7 @@
267
125
 
268
126
  <nav class="flex items-center space-x-1">
269
127
  <% if current_page > 1 %>
270
- <%= link_to "Previous", url_for(page: current_page - 1), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
128
+ <%= link_to "Previous", url_for(request.query_parameters.merge(page: current_page - 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
271
129
  <% else %>
272
130
  <span
273
131
  class="
@@ -307,12 +165,12 @@
307
165
  <%= page %>
308
166
  </span>
309
167
  <% else %>
310
- <%= link_to page, url_for(page: page), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
168
+ <%= link_to page, url_for(request.query_parameters.merge(page: page)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
311
169
  <% end %>
312
170
  <% end %>
313
171
 
314
172
  <% if current_page < total_pages %>
315
- <%= link_to "Next", url_for(page: current_page + 1), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
173
+ <%= link_to "Next", url_for(request.query_parameters.merge(page: current_page + 1)), class: "px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-600 rounded-md hover:bg-gray-50 dark:hover:bg-gray-700" %>
316
174
  <% else %>
317
175
  <span
318
176
  class="
@@ -9,20 +9,9 @@
9
9
  <!-- Header -->
10
10
  <div class="px-4 py-3 border-b border-gray-200 dark:border-gray-700 flex items-center justify-between">
11
11
  <div class="flex items-center gap-2">
12
- <% case execution.workflow_type
13
- when "pipeline" %>
14
- <span class="text-indigo-600 dark:text-indigo-400">→</span>
15
- <span class="font-medium text-gray-900 dark:text-gray-100">Pipeline</span>
16
- <span class="text-gray-500 dark:text-gray-400">· <%= stats[:steps_count] %> steps</span>
17
- <% when "parallel" %>
18
- <span class="text-cyan-600 dark:text-cyan-400">⫴</span>
19
- <span class="font-medium text-gray-900 dark:text-gray-100">Parallel</span>
20
- <span class="text-gray-500 dark:text-gray-400">· <%= stats[:steps_count] %> branches</span>
21
- <% when "router" %>
22
- <span class="text-amber-600 dark:text-amber-400">⑂</span>
23
- <span class="font-medium text-gray-900 dark:text-gray-100">Router</span>
24
- <span class="text-gray-500 dark:text-gray-400">→ <%= execution.routed_to %></span>
25
- <% end %>
12
+ <span class="text-emerald-600 dark:text-emerald-400">◈</span>
13
+ <span class="font-medium text-gray-900 dark:text-gray-100">Workflow</span>
14
+ <span class="text-gray-500 dark:text-gray-400">· <%= stats[:steps_count] %> steps</span>
26
15
  </div>
27
16
 
28
17
  <% overall_status = execution.workflow_overall_status %>
@@ -34,9 +23,7 @@
34
23
  <table class="min-w-full text-sm">
35
24
  <thead class="bg-gray-50 dark:bg-gray-900/50">
36
25
  <tr>
37
- <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">
38
- <%= execution.pipeline_workflow? ? "Step" : execution.parallel_workflow? ? "Branch" : "Route" %>
39
- </th>
26
+ <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Step</th>
40
27
  <th class="px-4 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Status</th>
41
28
  <th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Duration</th>
42
29
  <th class="px-4 py-2 text-right text-xs font-medium text-gray-500 dark:text-gray-400 uppercase">Tokens</th>
@@ -48,9 +35,7 @@
48
35
  <tr class="hover:bg-gray-50 dark:hover:bg-gray-800/50">
49
36
  <td class="px-4 py-2">
50
37
  <%= link_to ruby_llm_agents.execution_path(step.id), class: "text-blue-600 dark:text-blue-400 hover:underline" do %>
51
- <% if execution.pipeline_workflow? %>
52
- <span class="text-gray-400 dark:text-gray-500"><%= index + 1 %>.</span>
53
- <% end %>
38
+ <span class="text-gray-400 dark:text-gray-500"><%= index + 1 %>.</span>
54
39
  <%= step.workflow_step || step.agent_type.gsub(/Agent$/, "") %>
55
40
  <% end %>
56
41
  </td>
@@ -1,88 +1,12 @@
1
- <h2 class="text-2xl font-bold text-gray-900 dark:text-gray-100 mb-6">Executions</h2>
2
-
3
- <div id="executions_content" x-data="{ activeTab: '<%= params[:execution_type] || 'all' %>', activeWorkflowType: '<%= params[:workflow_type_tab] %>' }">
4
- <!-- Top-Level Execution Type Tabs -->
5
- <div class="border-b border-gray-200 dark:border-gray-700 mb-4">
6
- <nav class="-mb-px flex space-x-6" aria-label="Execution Types">
7
- <button type="button" @click="activeTab = 'all'; activeWorkflowType = ''; submitFilters()"
8
- :class="activeTab === 'all' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
9
- class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
10
- All
11
- </button>
12
- <button type="button" @click="activeTab = 'agents'; activeWorkflowType = ''; submitFilters()"
13
- :class="activeTab === 'agents' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
14
- class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
15
- Agents Only
16
- </button>
17
- <button type="button" @click="activeTab = 'workflows'; activeWorkflowType = ''; submitFilters()"
18
- :class="activeTab === 'workflows' ? 'border-blue-500 text-blue-600 dark:text-blue-400' : 'border-transparent text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-300'"
19
- class="whitespace-nowrap py-3 px-1 border-b-2 font-medium text-sm transition-colors">
20
- Workflows
21
- </button>
22
- </nav>
23
- </div>
24
-
25
- <!-- Workflow Sub-tabs -->
26
- <div x-show="activeTab === 'workflows'" x-cloak class="mb-4">
27
- <div class="flex flex-wrap gap-2">
28
- <button type="button" @click="activeWorkflowType = ''; submitFilters()"
29
- :class="activeWorkflowType === '' ? 'bg-gray-900 dark:bg-gray-100 text-white dark:text-gray-900' : 'bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600'"
30
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors">
31
- All Workflows
32
- </button>
33
- <button type="button" @click="activeWorkflowType = 'pipeline'; submitFilters()"
34
- :class="activeWorkflowType === 'pipeline' ? 'bg-indigo-600 text-white' : 'bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300 hover:bg-indigo-200 dark:hover:bg-indigo-800'"
35
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
36
- <span aria-hidden="true">-></span> Pipeline
37
- </button>
38
- <button type="button" @click="activeWorkflowType = 'parallel'; submitFilters()"
39
- :class="activeWorkflowType === 'parallel' ? 'bg-cyan-600 text-white' : 'bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300 hover:bg-cyan-200 dark:hover:bg-cyan-800'"
40
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
41
- <span aria-hidden="true">//</span> Parallel
42
- </button>
43
- <button type="button" @click="activeWorkflowType = 'router'; submitFilters()"
44
- :class="activeWorkflowType === 'router' ? 'bg-amber-600 text-white' : 'bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800'"
45
- class="px-3 py-1.5 rounded-md text-sm font-medium transition-colors inline-flex items-center gap-1.5">
46
- <span aria-hidden="true">&lt;&gt;</span> Router
47
- </button>
48
- </div>
1
+ <div class="mb-6">
2
+ <div class="flex items-center gap-2">
3
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Executions</h1>
4
+ <%= render "ruby_llm/agents/shared/doc_link" %>
49
5
  </div>
6
+ <p class="text-gray-500 dark:text-gray-400 mt-1">All agent and workflow execution history</p>
7
+ </div>
50
8
 
51
- <!-- Hidden inputs for tab state -->
52
- <input type="hidden" id="execution-type-filter" name="execution_type" x-model="activeTab" form="filters-form">
53
- <input type="hidden" id="workflow-type-tab-filter" name="workflow_type_tab" x-model="activeWorkflowType" form="filters-form">
54
-
9
+ <div id="executions_content">
55
10
  <%= render partial: "ruby_llm/agents/executions/filters", locals: { agent_types: @agent_types, model_ids: @model_ids, workflow_types: @workflow_types, filter_stats: @filter_stats } %>
56
11
  <%= render partial: "ruby_llm/agents/executions/list", locals: { executions: @executions, pagination: @pagination, filter_stats: @filter_stats } %>
57
12
  </div>
58
-
59
- <script>
60
- // Toggle attempts expansion (used by executions list)
61
- function toggleAttempts(executionId) {
62
- const attemptsRow = document.getElementById('attempts-row-' + executionId);
63
- const expandBtn = document.querySelector(`button[data-execution-id="${executionId}"]`);
64
-
65
- if (attemptsRow) {
66
- const isHidden = attemptsRow.classList.contains('hidden');
67
- attemptsRow.classList.toggle('hidden');
68
-
69
- // Rotate the chevron
70
- if (expandBtn) {
71
- const svg = expandBtn.querySelector('svg');
72
- if (svg) {
73
- svg.classList.toggle('rotate-90', isHidden);
74
- }
75
- expandBtn.setAttribute('aria-expanded', isHidden ? 'true' : 'false');
76
- }
77
- }
78
- }
79
-
80
- // Submit filters when tabs change
81
- function submitFilters() {
82
- // Delay to allow Alpine to update x-model values
83
- setTimeout(function() {
84
- const form = document.getElementById('filters-form');
85
- if (form) form.requestSubmit();
86
- }, 50);
87
- }
88
- </script>