ruby_llm-agents 0.3.1 → 0.3.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +68 -4
  4. data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
  5. data/app/models/ruby_llm/agents/execution/scopes.rb +10 -0
  6. data/app/models/ruby_llm/agents/execution.rb +26 -58
  7. data/app/views/layouts/rubyllm/agents/application.html.erb +103 -352
  8. data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
  9. data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
  10. data/app/views/rubyllm/agents/agents/show.html.erb +349 -416
  11. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
  12. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
  13. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
  14. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +54 -39
  15. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
  16. data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
  17. data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -151
  18. data/app/views/rubyllm/agents/executions/show.html.erb +256 -93
  19. data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
  20. data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
  21. data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
  22. data/config/routes.rb +2 -0
  23. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
  24. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
  25. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  26. data/lib/ruby_llm/agents/base/caching.rb +43 -0
  27. data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
  28. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  29. data/lib/ruby_llm/agents/base/execution.rb +206 -0
  30. data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -0
  31. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  32. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  33. data/lib/ruby_llm/agents/base.rb +19 -619
  34. data/lib/ruby_llm/agents/instrumentation.rb +36 -3
  35. data/lib/ruby_llm/agents/result.rb +235 -0
  36. data/lib/ruby_llm/agents/version.rb +1 -1
  37. data/lib/ruby_llm/agents.rb +1 -0
  38. metadata +15 -20
  39. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  40. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  41. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  42. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  43. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
@@ -1,23 +1,8 @@
1
- <div class="mb-6">
2
- <%= link_to ruby_llm_agents.agents_path, class: "text-blue-600 dark:text-blue-400 hover:underline" do %>
3
- <span class="inline-flex items-center">
4
- <svg
5
- class="w-4 h-4 mr-1"
6
- fill="none"
7
- stroke="currentColor"
8
- viewBox="0 0 24 24"
9
- >
10
- <path
11
- stroke-linecap="round"
12
- stroke-linejoin="round"
13
- stroke-width="2"
14
- d="M10 19l-7-7m0 0l7-7m-7 7h18"
15
- />
16
- </svg>
17
- Back to Agents
18
- </span>
19
- <% end %>
20
- </div>
1
+ <%= render "rubyllm/agents/shared/breadcrumbs", items: [
2
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
3
+ { label: "Agents", path: ruby_llm_agents.agents_path },
4
+ { label: @agent_type.gsub(/Agent$/, '') }
5
+ ] %>
21
6
 
22
7
  <!-- Header -->
23
8
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
@@ -49,7 +34,9 @@
49
34
  <% end %>
50
35
 
51
36
  <% if @config %>
52
- <span class="text-sm text-gray-500 dark:text-gray-400">v<%= @config[:version] %></span>
37
+ <span class="text-sm text-gray-500 dark:text-gray-400">
38
+ v<%= @config[:version] %>
39
+ </span>
53
40
  <% end %>
54
41
  </div>
55
42
 
@@ -61,8 +48,29 @@
61
48
  <% end %>
62
49
  </div>
63
50
 
64
- <div class="text-right text-sm text-gray-500 dark:text-gray-400">
65
- <p><%= @stats[:count] %> total executions</p>
51
+ <div class="text-right">
52
+ <p class="text-sm text-gray-500 dark:text-gray-400">
53
+ <%= number_with_delimiter(@stats[:count]) %> total executions
54
+ </p>
55
+
56
+ <div class="flex items-center justify-end gap-3 mt-1">
57
+ <% status_colors = {
58
+ "success" => "bg-green-500",
59
+ "error" => "bg-red-500",
60
+ "timeout" => "bg-yellow-500",
61
+ "running" => "bg-blue-500"
62
+ } %>
63
+
64
+ <% @status_distribution.each do |status, count| %>
65
+ <div class="flex items-center gap-1">
66
+ <span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
67
+
68
+ <span class="text-xs text-gray-600 dark:text-gray-400">
69
+ <%= number_with_delimiter(count) %>
70
+ </span>
71
+ </div>
72
+ <% end %>
73
+ </div>
66
74
  </div>
67
75
  </div>
68
76
  </div>
@@ -71,8 +79,18 @@
71
79
  <% if @config && @circuit_breaker_status.present? %>
72
80
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
73
81
  <div class="flex items-center justify-between mb-3">
74
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Circuit Breaker Status</h3>
75
- <span class="text-xs text-gray-400 dark:text-gray-500">Auto-refreshes every 30s</span>
82
+ <h3
83
+ class="
84
+ text-sm font-medium text-gray-700 dark:text-gray-300 uppercase
85
+ tracking-wider
86
+ "
87
+ >
88
+ Circuit Breaker Status
89
+ </h3>
90
+
91
+ <span class="text-xs text-gray-400 dark:text-gray-500">
92
+ Auto-refreshes every 30s
93
+ </span>
76
94
  </div>
77
95
 
78
96
  <div class="flex flex-wrap gap-3">
@@ -81,11 +99,21 @@
81
99
  <% if status[:open] %>
82
100
  <!-- Open/Tripped indicator -->
83
101
  <span class="relative flex h-3 w-3">
84
- <span class="animate-ping absolute inline-flex h-full w-full rounded-full bg-red-400 opacity-75"></span>
102
+ <span
103
+ class="
104
+ animate-ping absolute inline-flex h-full w-full
105
+ rounded-full bg-red-400 opacity-75
106
+ "
107
+ ></span>
108
+
85
109
  <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
86
110
  </span>
87
- <span class="text-sm font-medium text-red-700 dark:text-red-300"><%= model_id %></span>
88
- <span class="text-xs text-red-500 dark:text-red-400 ml-1">OPEN</span>
111
+ <span class="text-sm font-medium text-red-700 dark:text-red-300">
112
+ <%= model_id %>
113
+ </span>
114
+ <span class="text-xs text-red-500 dark:text-red-400 ml-1">
115
+ OPEN
116
+ </span>
89
117
  <% if status[:cooldown_remaining] %>
90
118
  <span class="text-xs text-red-400 dark:text-red-500 ml-1">
91
119
  (resets in <%= status[:cooldown_remaining] %>s)
@@ -94,11 +122,16 @@
94
122
  <% else %>
95
123
  <!-- Closed/Healthy indicator -->
96
124
  <span class="inline-flex rounded-full h-3 w-3 bg-green-500"></span>
97
- <span class="text-sm font-medium text-green-700 dark:text-green-300"><%= model_id %></span>
98
- <span class="text-xs text-green-500 dark:text-green-400 ml-1">OK</span>
125
+ <span class="text-sm font-medium text-green-700 dark:text-green-300">
126
+ <%= model_id %>
127
+ </span>
128
+ <span class="text-xs text-green-500 dark:text-green-400 ml-1">
129
+ OK
130
+ </span>
99
131
  <% if status[:failure_count] && status[:failure_count] > 0 %>
100
132
  <span class="text-xs text-yellow-500 dark:text-yellow-400 ml-1">
101
- (<%= status[:failure_count] %>/<%= status[:threshold] %> failures)
133
+ (<%= status[:failure_count] %>/<%= status[:threshold] %>
134
+ failures)
102
135
  </span>
103
136
  <% end %>
104
137
  <% end %>
@@ -108,8 +141,8 @@
108
141
 
109
142
  <% if @circuit_breaker_status.values.any? { |s| s[:open] } %>
110
143
  <p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
111
- Open circuit breakers will skip the model and try fallbacks (if configured).
112
- They automatically reset after the cooldown period.
144
+ Open circuit breakers will skip the model and try fallbacks (if
145
+ configured). They automatically reset after the cooldown period.
113
146
  </p>
114
147
  <% end %>
115
148
  </div>
@@ -118,6 +151,7 @@
118
151
  <!-- Stats Grid -->
119
152
  <% success_rate = @stats[:success_rate] || 0 %>
120
153
  <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
154
+
121
155
  <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
122
156
  <%= render "rubyllm/agents/shared/stat_card",
123
157
  title: "Executions",
@@ -164,77 +198,60 @@
164
198
  <!-- Charts Section -->
165
199
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
166
200
  <!-- Executions Over Time -->
167
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 pb-20 overflow-hidden">
201
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
168
202
  <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
169
203
  Executions (30 days)
170
204
  </h3>
171
-
172
- <div id="executions-chart" style="height: 250px;">
173
- <% success_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:count] - (d[:error_count] || 0)] }.to_h
174
- failed_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:error_count] || 0] }.to_h %>
175
-
176
- <%= area_chart [
177
- { name: "Success", data: success_data },
178
- { name: "Failed", data: failed_data }
179
- ], colors: ["#10B981", "#EF4444"], stacked: true, library: {
180
- yAxis: { min: 0 },
181
- legend: { align: "center", verticalAlign: "bottom" },
182
- plotOptions: { area: { stacking: "normal" } }
183
- } %>
184
- </div>
205
+ <div id="executions-chart" style="height: 220px;"></div>
185
206
  </div>
186
207
 
187
208
  <!-- Cost Over Time -->
188
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 pb-8 overflow-hidden">
189
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Cost (30 days)</h3>
190
-
191
- <div id="cost-chart" style="height: 250px;">
192
- <% cost_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:total_cost].to_f.round(4)] }.to_h %>
193
-
194
- <%= line_chart cost_data, colors: ["#10B981"], prefix: "$", library: {
195
- yAxis: { min: 0 },
196
- legend: { enabled: false }
197
- } %>
198
- </div>
209
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 overflow-hidden">
210
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
211
+ Cost (30 days)
212
+ </h3>
213
+ <div id="cost-chart" style="height: 220px;"></div>
199
214
  </div>
200
215
  </div>
201
216
 
202
- <!-- Status Distribution (compact) -->
203
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
204
- <div class="flex items-center justify-between">
205
- <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">Status Distribution</p>
206
-
207
- <div class="flex flex-wrap gap-4">
208
- <% status_colors = {
209
- "success" => "#10B981",
210
- "error" => "#EF4444",
211
- "timeout" => "#F59E0B",
212
- "running" => "#3B82F6"
213
- } %>
214
-
215
- <% @status_distribution.each do |status, count| %>
216
- <div class="flex items-center">
217
- <span
218
- class="w-2 h-2 rounded-full mr-1.5"
219
- style="background-color: <%= status_colors[status] || '#6B7280' %>"
220
- ></span>
221
-
222
- <span class="text-sm text-gray-700 dark:text-gray-300 capitalize"><%= status %></span>
217
+ <script>
218
+ (function() {
219
+ const trendData = <%= raw @trend_data.to_json %>;
220
+ const categories = trendData.map(d => d.date.split('-').slice(1).join('/'));
221
+
222
+ // Executions chart
223
+ Highcharts.chart('executions-chart', {
224
+ chart: { type: 'areaspline', backgroundColor: 'transparent' },
225
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
226
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' } } },
227
+ legend: { enabled: false },
228
+ plotOptions: { areaspline: { fillOpacity: 0.2, marker: { enabled: false } } },
229
+ series: [
230
+ { name: 'Success', color: '#10B981', data: trendData.map(d => d.count - (d.error_count || 0)) },
231
+ { name: 'Failed', color: '#EF4444', data: trendData.map(d => d.error_count || 0) }
232
+ ]
233
+ });
223
234
 
224
- <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
225
- (<%= number_with_delimiter(count) %>)
226
- </span>
227
- </div>
228
- <% end %>
229
- </div>
230
- </div>
231
- </div>
235
+ // Cost chart
236
+ Highcharts.chart('cost-chart', {
237
+ chart: { type: 'spline', backgroundColor: 'transparent' },
238
+ xAxis: { categories: categories, labels: { style: { color: '#9CA3AF' } } },
239
+ yAxis: { min: 0, title: { text: null }, labels: { style: { color: '#9CA3AF' }, format: '${value}' } },
240
+ legend: { enabled: false },
241
+ plotOptions: { spline: { marker: { enabled: false } } },
242
+ tooltip: { valuePrefix: '$' },
243
+ series: [{ name: 'Cost', color: '#10B981', data: trendData.map(d => parseFloat(d.total_cost) || 0) }]
244
+ });
245
+ })();
246
+ </script>
232
247
 
233
248
  <!-- Finish Reason Distribution -->
234
249
  <% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
235
250
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
236
251
  <div class="flex items-center justify-between">
237
- <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">Finish Reasons</p>
252
+ <p class="text-sm text-gray-500 dark:text-gray-400 uppercase">
253
+ Finish Reasons
254
+ </p>
238
255
 
239
256
  <div class="flex flex-wrap gap-4">
240
257
  <% finish_colors = {
@@ -252,7 +269,9 @@
252
269
  style="background-color: <%= finish_colors[reason] || '#6B7280' %>"
253
270
  ></span>
254
271
 
255
- <span class="text-sm text-gray-700 dark:text-gray-300"><%= reason || 'unknown' %></span>
272
+ <span class="text-sm text-gray-700 dark:text-gray-300">
273
+ <%= reason || 'unknown' %>
274
+ </span>
256
275
 
257
276
  <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
258
277
  (<%= number_with_delimiter(count) %>)
@@ -271,31 +290,48 @@
271
290
  <% if @config %>
272
291
  <!-- Configuration -->
273
292
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
274
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Configuration</h3>
293
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
294
+ Configuration
295
+ </h3>
275
296
 
276
297
  <!-- Basic Configuration -->
277
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Basic</p>
298
+ <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">
299
+ Basic
300
+ </p>
301
+
278
302
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
279
303
  <div>
280
304
  <p class="text-sm text-gray-500 dark:text-gray-400">Model</p>
281
- <p class="font-medium text-gray-900 dark:text-gray-100"><%= @config[:model] %></p>
305
+
306
+ <p class="font-medium text-gray-900 dark:text-gray-100">
307
+ <%= @config[:model] %>
308
+ </p>
282
309
  </div>
283
310
 
284
311
  <div>
285
312
  <p class="text-sm text-gray-500 dark:text-gray-400">Temperature</p>
286
- <p class="font-medium text-gray-900 dark:text-gray-100"><%= @config[:temperature] %></p>
313
+
314
+ <p class="font-medium text-gray-900 dark:text-gray-100">
315
+ <%= @config[:temperature] %>
316
+ </p>
287
317
  </div>
288
318
 
289
319
  <div>
290
320
  <p class="text-sm text-gray-500 dark:text-gray-400">Timeout</p>
291
- <p class="font-medium text-gray-900 dark:text-gray-100"><%= @config[:timeout] %> seconds</p>
321
+
322
+ <p class="font-medium text-gray-900 dark:text-gray-100">
323
+ <%= @config[:timeout] %> seconds
324
+ </p>
292
325
  </div>
293
326
 
294
327
  <div>
295
328
  <p class="text-sm text-gray-500 dark:text-gray-400">Cache</p>
329
+
296
330
  <p class="font-medium text-gray-900 dark:text-gray-100">
297
331
  <% if @config[:cache_enabled] %>
298
- Enabled (<%= @config[:cache_ttl].inspect %>)
332
+ Enabled (
333
+ <%= @config[:cache_ttl].inspect %>
334
+ )
299
335
  <% else %>
300
336
  <span class="text-gray-400 dark:text-gray-500">Disabled</span>
301
337
  <% end %>
@@ -304,16 +340,22 @@
304
340
  </div>
305
341
 
306
342
  <!-- Reliability Configuration -->
307
- <%
308
- retries_config = @config[:retries] || {}
343
+ <% retries_config = @config[:retries] || {}
309
344
  has_retries = (retries_config[:max] || 0) > 0
310
345
  has_fallbacks = @config[:fallback_models].present? && @config[:fallback_models].any?
311
346
  has_total_timeout = @config[:total_timeout].present?
312
347
  has_circuit_breaker = @config[:circuit_breaker].present?
313
- has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker
314
- %>
348
+ has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker %>
349
+
315
350
  <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mb-6">
316
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Reliability</p>
351
+ <p
352
+ class="
353
+ text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider
354
+ mb-3
355
+ "
356
+ >
357
+ Reliability
358
+ </p>
317
359
 
318
360
  <% if has_any_reliability %>
319
361
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -321,26 +363,48 @@
321
363
  <div class="flex items-start gap-3 p-3 rounded-lg <%= has_retries ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
322
364
  <div class="flex-shrink-0 mt-0.5">
323
365
  <% if has_retries %>
324
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
325
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
366
+ <svg
367
+ class="w-5 h-5 text-green-500"
368
+ fill="currentColor"
369
+ viewBox="0 0 20 20"
370
+ >
371
+ <path
372
+ fill-rule="evenodd"
373
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
374
+ clip-rule="evenodd"
375
+ />
326
376
  </svg>
327
377
  <% else %>
328
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
329
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
378
+ <svg
379
+ class="w-5 h-5 text-gray-400"
380
+ fill="currentColor"
381
+ viewBox="0 0 20 20"
382
+ >
383
+ <path
384
+ fill-rule="evenodd"
385
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
386
+ clip-rule="evenodd"
387
+ />
330
388
  </svg>
331
389
  <% end %>
332
390
  </div>
391
+
333
392
  <div>
334
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Retries</p>
393
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
394
+ Retries
395
+ </p>
396
+
335
397
  <% if has_retries %>
336
398
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
337
- Max: <%= retries_config[:max] %> &middot;
338
- Backoff: <%= retries_config[:backoff] %> &middot;
339
- Base: <%= retries_config[:base] %>s &middot;
340
- Max delay: <%= retries_config[:max_delay] %>s
399
+ Max: <%= retries_config[:max] %> &middot; Backoff:
400
+ <%= retries_config[:backoff] %> &middot; Base:
401
+ <%= retries_config[:base] %>s &middot; Max delay:
402
+ <%= retries_config[:max_delay] %>s
341
403
  </p>
342
404
  <% else %>
343
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
405
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
406
+ Not configured
407
+ </p>
344
408
  <% end %>
345
409
  </div>
346
410
  </div>
@@ -349,23 +413,45 @@
349
413
  <div class="flex items-start gap-3 p-3 rounded-lg <%= has_fallbacks ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
350
414
  <div class="flex-shrink-0 mt-0.5">
351
415
  <% if has_fallbacks %>
352
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
353
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
416
+ <svg
417
+ class="w-5 h-5 text-green-500"
418
+ fill="currentColor"
419
+ viewBox="0 0 20 20"
420
+ >
421
+ <path
422
+ fill-rule="evenodd"
423
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
424
+ clip-rule="evenodd"
425
+ />
354
426
  </svg>
355
427
  <% else %>
356
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
357
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
428
+ <svg
429
+ class="w-5 h-5 text-gray-400"
430
+ fill="currentColor"
431
+ viewBox="0 0 20 20"
432
+ >
433
+ <path
434
+ fill-rule="evenodd"
435
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
436
+ clip-rule="evenodd"
437
+ />
358
438
  </svg>
359
439
  <% end %>
360
440
  </div>
441
+
361
442
  <div>
362
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Fallback Models</p>
443
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
444
+ Fallback Models
445
+ </p>
446
+
363
447
  <% if has_fallbacks %>
364
448
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
365
449
  <%= @config[:fallback_models].join(" → ") %>
366
450
  </p>
367
451
  <% else %>
368
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
452
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
453
+ Not configured
454
+ </p>
369
455
  <% end %>
370
456
  </div>
371
457
  </div>
@@ -374,23 +460,45 @@
374
460
  <div class="flex items-start gap-3 p-3 rounded-lg <%= has_total_timeout ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
375
461
  <div class="flex-shrink-0 mt-0.5">
376
462
  <% if has_total_timeout %>
377
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
378
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
463
+ <svg
464
+ class="w-5 h-5 text-green-500"
465
+ fill="currentColor"
466
+ viewBox="0 0 20 20"
467
+ >
468
+ <path
469
+ fill-rule="evenodd"
470
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
471
+ clip-rule="evenodd"
472
+ />
379
473
  </svg>
380
474
  <% else %>
381
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
382
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
475
+ <svg
476
+ class="w-5 h-5 text-gray-400"
477
+ fill="currentColor"
478
+ viewBox="0 0 20 20"
479
+ >
480
+ <path
481
+ fill-rule="evenodd"
482
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
483
+ clip-rule="evenodd"
484
+ />
383
485
  </svg>
384
486
  <% end %>
385
487
  </div>
488
+
386
489
  <div>
387
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Total Timeout</p>
490
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
491
+ Total Timeout
492
+ </p>
493
+
388
494
  <% if has_total_timeout %>
389
495
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
390
496
  <%= @config[:total_timeout] %> seconds across all attempts
391
497
  </p>
392
498
  <% else %>
393
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
499
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
500
+ Not configured
501
+ </p>
394
502
  <% end %>
395
503
  </div>
396
504
  </div>
@@ -399,43 +507,79 @@
399
507
  <div class="flex items-start gap-3 p-3 rounded-lg <%= has_circuit_breaker ? 'bg-green-50 dark:bg-green-900/20' : 'bg-gray-50 dark:bg-gray-700/50' %>">
400
508
  <div class="flex-shrink-0 mt-0.5">
401
509
  <% if has_circuit_breaker %>
402
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
403
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
510
+ <svg
511
+ class="w-5 h-5 text-green-500"
512
+ fill="currentColor"
513
+ viewBox="0 0 20 20"
514
+ >
515
+ <path
516
+ fill-rule="evenodd"
517
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z"
518
+ clip-rule="evenodd"
519
+ />
404
520
  </svg>
405
521
  <% else %>
406
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
407
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
522
+ <svg
523
+ class="w-5 h-5 text-gray-400"
524
+ fill="currentColor"
525
+ viewBox="0 0 20 20"
526
+ >
527
+ <path
528
+ fill-rule="evenodd"
529
+ d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
530
+ clip-rule="evenodd"
531
+ />
408
532
  </svg>
409
533
  <% end %>
410
534
  </div>
535
+
411
536
  <div>
412
- <p class="text-sm font-medium text-gray-900 dark:text-gray-100">Circuit Breaker</p>
537
+ <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
538
+ Circuit Breaker
539
+ </p>
540
+
413
541
  <% if has_circuit_breaker %>
414
542
  <% cb = @config[:circuit_breaker] %>
415
543
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
416
- Opens after <%= cb[:errors] %> errors within <%= cb[:within] %>s &middot;
417
- Cooldown: <%= cb[:cooldown] %>s
544
+ Opens after <%= cb[:errors] %> errors within
545
+ <%= cb[:within] %>s &middot; Cooldown: <%= cb[:cooldown] %>s
418
546
  </p>
419
547
  <% else %>
420
- <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">Not configured</p>
548
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
549
+ Not configured
550
+ </p>
421
551
  <% end %>
422
552
  </div>
423
553
  </div>
424
554
  </div>
425
555
  <% else %>
426
- <p class="text-sm text-gray-400 dark:text-gray-500">No reliability features configured</p>
556
+ <p class="text-sm text-gray-400 dark:text-gray-500">
557
+ No reliability features configured
558
+ </p>
427
559
  <% end %>
428
560
  </div>
429
561
 
430
562
  <!-- Parameters -->
431
563
  <% if @config[:params].present? && @config[:params].any? %>
432
564
  <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
433
- <p class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-3">Parameters</p>
565
+ <p
566
+ class="
567
+ text-xs text-gray-500 dark:text-gray-400 uppercase
568
+ tracking-wider mb-3
569
+ "
570
+ >
571
+ Parameters
572
+ </p>
434
573
 
435
574
  <div class="space-y-2">
436
575
  <% @config[:params].each do |name, opts| %>
437
576
  <div class="flex items-center text-sm">
438
- <code class="bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2 py-0.5 rounded font-mono">
577
+ <code
578
+ class="
579
+ bg-gray-100 dark:bg-gray-700 dark:text-gray-200 px-2
580
+ py-0.5 rounded font-mono
581
+ "
582
+ >
439
583
  <%= name %>
440
584
  </code>
441
585
 
@@ -448,7 +592,9 @@
448
592
  default: <%= opts[:default].inspect %>
449
593
  </span>
450
594
  <% else %>
451
- <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">optional</span>
595
+ <span class="ml-2 text-xs text-gray-400 dark:text-gray-500">
596
+ optional
597
+ </span>
452
598
  <% end %>
453
599
  </div>
454
600
  <% end %>
@@ -460,7 +606,9 @@
460
606
 
461
607
  <!-- Executions -->
462
608
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
463
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Executions</h3>
609
+ <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
610
+ Executions
611
+ </h3>
464
612
 
465
613
  <%= turbo_frame_tag "executions_table" do %>
466
614
  <%
@@ -469,217 +617,84 @@
469
617
  selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
470
618
  selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
471
619
  selected_temperatures = params[:temperatures].present? ? (params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")).map(&:to_s) : []
620
+
621
+ status_options = [
622
+ { value: "success", label: "Success", color: "bg-green-500" },
623
+ { value: "error", label: "Error", color: "bg-red-500" },
624
+ { value: "running", label: "Running", color: "bg-blue-500" },
625
+ { value: "timeout", label: "Timeout", color: "bg-yellow-500" }
626
+ ]
627
+ version_options = @versions.map { |v| { value: v.to_s, label: "v#{v}" } }
628
+ model_options = @models.map { |m| { value: m, label: m } }
629
+ temperature_options = @temperatures.map { |t| { value: t.to_s, label: t.to_s } }
630
+ days_options = [
631
+ { value: "", label: "All Time" },
632
+ { value: "1", label: "Today" },
633
+ { value: "7", label: "Last 7 Days" },
634
+ { value: "30", label: "Last 30 Days" }
635
+ ]
472
636
  %>
473
637
 
474
- <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, data: { turbo_frame: "executions_table" }, id: "agent-filters-form" do |f| %>
638
+ <%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, data: { turbo_frame: "executions_table" } do |f| %>
475
639
  <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
476
- <!-- Status Filter (Multi-select) -->
477
- <div class="relative filter-dropdown" data-filter="statuses">
478
- <button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors <%= selected_statuses.any? ? 'ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800' : '' %>">
479
- <% if selected_statuses.length == 1
480
- status_color = case selected_statuses.first
481
- when 'success' then 'bg-green-500'
482
- when 'error' then 'bg-red-500'
483
- when 'running' then 'bg-blue-500'
484
- when 'timeout' then 'bg-yellow-500'
485
- else 'bg-gray-400'
486
- end
487
- else
488
- status_color = 'bg-gray-400'
489
- end %>
490
- <span class="w-2 h-2 rounded-full <%= status_color %>"></span>
491
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
492
- <% if selected_statuses.empty? %>
493
- All Statuses
494
- <% elsif selected_statuses.length == 1 %>
495
- <%= selected_statuses.first.capitalize %>
496
- <% else %>
497
- <%= selected_statuses.length %> Statuses
498
- <% end %>
499
- </span>
500
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
501
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
502
- </svg>
503
- </button>
504
- <div class="dropdown-menu hidden absolute z-10 mt-1 w-44 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 py-1">
505
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
506
- <label class="flex items-center gap-2 cursor-pointer">
507
- <input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="toggleAllOptions(this, 'statuses')" <%= selected_statuses.empty? ? 'checked' : '' %>>
508
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Statuses</span>
509
- </label>
510
- </div>
511
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
512
- <input type="checkbox" name="statuses[]" value="success" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('success') ? 'checked' : '' %>>
513
- <span class="w-2 h-2 rounded-full bg-green-500"></span>
514
- Success
515
- </label>
516
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
517
- <input type="checkbox" name="statuses[]" value="error" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('error') ? 'checked' : '' %>>
518
- <span class="w-2 h-2 rounded-full bg-red-500"></span>
519
- Error
520
- </label>
521
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
522
- <input type="checkbox" name="statuses[]" value="running" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('running') ? 'checked' : '' %>>
523
- <span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
524
- Running
525
- </label>
526
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
527
- <input type="checkbox" name="statuses[]" value="timeout" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('timeout') ? 'checked' : '' %>>
528
- <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
529
- Timeout
530
- </label>
531
- </div>
532
- </div>
533
-
534
- <!-- Version Filter (Multi-select) -->
640
+ <%# Status Filter (Multi-select) %>
641
+ <%= render "rubyllm/agents/shared/filter_dropdown",
642
+ name: "statuses[]",
643
+ filter_id: "statuses",
644
+ label: "Status",
645
+ all_label: "All Statuses",
646
+ options: status_options,
647
+ selected: selected_statuses %>
648
+
649
+ <%# Version Filter (Multi-select) %>
535
650
  <% if @versions.any? %>
536
- <div class="relative filter-dropdown" data-filter="versions">
537
- <button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors <%= selected_versions.any? ? 'ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800' : '' %>">
538
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
539
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z"/>
540
- </svg>
541
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
542
- <% if selected_versions.empty? %>
543
- All Versions
544
- <% elsif selected_versions.length == 1 %>
545
- v<%= selected_versions.first %>
546
- <% else %>
547
- <%= selected_versions.length %> Versions
548
- <% end %>
549
- </span>
550
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
551
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
552
- </svg>
553
- </button>
554
- <div class="dropdown-menu hidden absolute z-10 mt-1 w-44 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 py-1 max-h-64 overflow-y-auto">
555
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
556
- <label class="flex items-center gap-2 cursor-pointer">
557
- <input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="toggleAllOptions(this, 'versions')" <%= selected_versions.empty? ? 'checked' : '' %>>
558
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Versions</span>
559
- </label>
560
- </div>
561
- <% @versions.each do |version| %>
562
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
563
- <input type="checkbox" name="versions[]" value="<%= version %>" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('versions')" <%= selected_versions.include?(version.to_s) ? 'checked' : '' %>>
564
- v<%= version %>
565
- </label>
566
- <% end %>
567
- </div>
568
- </div>
651
+ <%= render "rubyllm/agents/shared/filter_dropdown",
652
+ name: "versions[]",
653
+ filter_id: "versions",
654
+ label: "Version",
655
+ all_label: "All Versions",
656
+ options: version_options,
657
+ selected: selected_versions,
658
+ icon: "M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" %>
569
659
  <% end %>
570
660
 
571
- <!-- Model Filter (Multi-select) -->
661
+ <%# Model Filter (Multi-select) %>
572
662
  <% if @models.length > 1 %>
573
- <div class="relative filter-dropdown" data-filter="models">
574
- <button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors <%= selected_models.any? ? 'ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800' : '' %>">
575
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
576
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>
577
- </svg>
578
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
579
- <% if selected_models.empty? %>
580
- All Models
581
- <% elsif selected_models.length == 1 %>
582
- <%= selected_models.first %>
583
- <% else %>
584
- <%= selected_models.length %> Models
585
- <% end %>
586
- </span>
587
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
588
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
589
- </svg>
590
- </button>
591
- <div class="dropdown-menu hidden absolute z-10 mt-1 w-56 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 py-1 max-h-64 overflow-y-auto">
592
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
593
- <label class="flex items-center gap-2 cursor-pointer">
594
- <input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="toggleAllOptions(this, 'models')" <%= selected_models.empty? ? 'checked' : '' %>>
595
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Models</span>
596
- </label>
597
- </div>
598
- <% @models.each do |model| %>
599
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
600
- <input type="checkbox" name="models[]" value="<%= model %>" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('models')" <%= selected_models.include?(model) ? 'checked' : '' %>>
601
- <%= model %>
602
- </label>
603
- <% end %>
604
- </div>
605
- </div>
663
+ <%= render "rubyllm/agents/shared/filter_dropdown",
664
+ name: "models[]",
665
+ filter_id: "models",
666
+ label: "Model",
667
+ all_label: "All Models",
668
+ options: model_options,
669
+ selected: selected_models,
670
+ icon: "M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" %>
606
671
  <% end %>
607
672
 
608
- <!-- Temperature Filter (Multi-select) -->
673
+ <%# Temperature Filter (Multi-select) %>
609
674
  <% if @temperatures.length > 1 %>
610
- <div class="relative filter-dropdown" data-filter="temperatures">
611
- <button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors <%= selected_temperatures.any? ? 'ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800' : '' %>">
612
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
613
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z"/>
614
- </svg>
615
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
616
- <% if selected_temperatures.empty? %>
617
- All Temps
618
- <% elsif selected_temperatures.length == 1 %>
619
- <%= selected_temperatures.first %>
620
- <% else %>
621
- <%= selected_temperatures.length %> Temps
622
- <% end %>
623
- </span>
624
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
625
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
626
- </svg>
627
- </button>
628
- <div class="dropdown-menu hidden absolute z-10 mt-1 w-36 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 py-1 max-h-64 overflow-y-auto">
629
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
630
- <label class="flex items-center gap-2 cursor-pointer">
631
- <input type="checkbox" class="select-all-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="toggleAllOptions(this, 'temperatures')" <%= selected_temperatures.empty? ? 'checked' : '' %>>
632
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Temps</span>
633
- </label>
634
- </div>
635
- <% @temperatures.each do |temp| %>
636
- <label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 cursor-pointer">
637
- <input type="checkbox" name="temperatures[]" value="<%= temp %>" class="filter-checkbox rounded border-gray-300 dark:border-gray-500 text-blue-600 focus:ring-blue-500 dark:bg-gray-600" onchange="updateMultiSelect('temperatures')" <%= selected_temperatures.include?(temp.to_s) ? 'checked' : '' %>>
638
- <%= temp %>
639
- </label>
640
- <% end %>
641
- </div>
642
- </div>
675
+ <%= render "rubyllm/agents/shared/filter_dropdown",
676
+ name: "temperatures[]",
677
+ filter_id: "temperatures",
678
+ label: "Temp",
679
+ all_label: "All Temps",
680
+ options: temperature_options,
681
+ selected: selected_temperatures,
682
+ icon: "M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" %>
643
683
  <% end %>
644
684
 
645
- <!-- Time Range Filter -->
646
- <div class="relative filter-dropdown" data-filter="days">
647
- <button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 dark:bg-gray-700 border border-gray-200 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-600 transition-colors <%= params[:days].present? ? 'ring-2 ring-blue-500 ring-offset-1 dark:ring-offset-gray-800' : '' %>">
648
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
649
- <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"/>
650
- </svg>
651
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
652
- <% case params[:days]
653
- when '1' then %>Today<%
654
- when '7' then %>Last 7 Days<%
655
- when '30' then %>Last 30 Days<%
656
- else %>All Time<%
657
- end %>
658
- </span>
659
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
660
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
661
- </svg>
662
- </button>
663
- <div class="dropdown-menu hidden absolute z-10 mt-1 w-40 bg-white dark:bg-gray-700 rounded-lg shadow-lg border border-gray-200 dark:border-gray-600 py-1">
664
- <a href="#" onclick="selectSingleFilter('days', '', 'All Time'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 <%= params[:days].blank? ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
665
- All Time
666
- </a>
667
- <a href="#" onclick="selectSingleFilter('days', '1', 'Today'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 <%= params[:days] == '1' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
668
- Today
669
- </a>
670
- <a href="#" onclick="selectSingleFilter('days', '7', 'Last 7 Days'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 <%= params[:days] == '7' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
671
- Last 7 Days
672
- </a>
673
- <a href="#" onclick="selectSingleFilter('days', '30', 'Last 30 Days'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-50 dark:hover:bg-gray-600 <%= params[:days] == '30' ? 'bg-blue-50 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300' : '' %>">
674
- Last 30 Days
675
- </a>
676
- </div>
677
- <%= f.hidden_field :days, value: params[:days], id: "filter_days" %>
678
- </div>
685
+ <%# Time Range Filter (Single-select) %>
686
+ <%= render "rubyllm/agents/shared/select_dropdown",
687
+ name: "days",
688
+ filter_id: "days",
689
+ options: days_options,
690
+ selected: params[:days].to_s,
691
+ icon: "M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" %>
679
692
 
680
- <!-- Clear Filters -->
693
+ <%# Clear Filters %>
681
694
  <% if has_filters %>
682
- <%= link_to ruby_llm_agents.agent_path(@agent_type), data: { turbo_frame: "executions_table" }, class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" do %>
695
+ <%= link_to ruby_llm_agents.agent_path(@agent_type),
696
+ data: { turbo_frame: "executions_table" },
697
+ class: "flex items-center gap-1 px-3 py-2 text-sm text-red-500 dark:text-red-400 hover:text-red-600 dark:hover:text-red-300 hover:bg-red-50 dark:hover:bg-red-900/30 rounded-lg transition-colors" do %>
683
698
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
684
699
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
685
700
  </svg>
@@ -687,7 +702,7 @@
687
702
  <% end %>
688
703
  <% end %>
689
704
 
690
- <!-- Stats Summary (right aligned) -->
705
+ <%# Stats Summary (right aligned) %>
691
706
  <div class="ml-auto flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
692
707
  <span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
693
708
  <span class="text-gray-300 dark:text-gray-600">|</span>
@@ -701,85 +716,3 @@
701
716
  <%= render "rubyllm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
702
717
  <% end %>
703
718
  </div>
704
-
705
- <script>
706
- function toggleDropdown(button) {
707
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
708
- if (menu !== button.nextElementSibling) {
709
- menu.classList.add('hidden');
710
- }
711
- });
712
- button.nextElementSibling.classList.toggle('hidden');
713
- }
714
-
715
- function selectSingleFilter(name, value, label) {
716
- document.getElementById('filter_' + name).value = value;
717
- const dropdown = document.querySelector(`[data-filter="${name}"]`);
718
- dropdown.querySelector('.dropdown-label').textContent = label;
719
- const button = dropdown.querySelector('button');
720
- if (value) {
721
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
722
- } else {
723
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
724
- }
725
- dropdown.querySelector('.dropdown-menu').classList.add('hidden');
726
- document.getElementById('agent-filters-form').requestSubmit();
727
- }
728
-
729
- function toggleAllOptions(checkbox, filterName) {
730
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
731
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox');
732
-
733
- if (checkbox.checked) {
734
- checkboxes.forEach(cb => cb.checked = false);
735
- updateMultiSelect(filterName);
736
- }
737
- }
738
-
739
- function updateMultiSelect(filterName) {
740
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
741
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox:checked');
742
- const selectAllCheckbox = dropdown.querySelector('.select-all-checkbox');
743
- const button = dropdown.querySelector('button');
744
- const label = dropdown.querySelector('.dropdown-label');
745
-
746
- const count = checkboxes.length;
747
-
748
- selectAllCheckbox.checked = (count === 0);
749
-
750
- const labelMap = {
751
- 'statuses': { all: 'All Statuses', plural: 'Statuses' },
752
- 'versions': { all: 'All Versions', plural: 'Versions' },
753
- 'models': { all: 'All Models', plural: 'Models' },
754
- 'temperatures': { all: 'All Temps', plural: 'Temps' }
755
- };
756
-
757
- if (count === 0) {
758
- label.textContent = labelMap[filterName]?.all || 'All';
759
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
760
- } else if (count === 1) {
761
- const value = checkboxes[0].value;
762
- if (filterName === 'statuses') {
763
- label.textContent = value.charAt(0).toUpperCase() + value.slice(1);
764
- } else if (filterName === 'versions') {
765
- label.textContent = 'v' + value;
766
- } else {
767
- label.textContent = value;
768
- }
769
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
770
- } else {
771
- label.textContent = `${count} ${labelMap[filterName]?.plural || 'Items'}`;
772
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
773
- }
774
-
775
- document.getElementById('agent-filters-form').requestSubmit();
776
- }
777
-
778
- document.addEventListener('click', function(e) {
779
- if (!e.target.closest('.filter-dropdown')) {
780
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
781
- menu.classList.add('hidden');
782
- });
783
- }
784
- });
785
- </script>