ruby_llm-agents 0.3.3 → 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 (31) hide show
  1. checksums.yaml +4 -4
  2. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +68 -4
  3. data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
  4. data/app/models/ruby_llm/agents/execution.rb +19 -58
  5. data/app/views/layouts/rubyllm/agents/application.html.erb +92 -350
  6. data/app/views/rubyllm/agents/agents/show.html.erb +331 -385
  7. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
  8. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
  9. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
  10. data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
  11. data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -121
  12. data/app/views/rubyllm/agents/executions/show.html.erb +134 -85
  13. data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
  14. data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
  15. data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
  16. data/config/routes.rb +2 -0
  17. data/lib/ruby_llm/agents/base/caching.rb +43 -0
  18. data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
  19. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  20. data/lib/ruby_llm/agents/base/execution.rb +206 -0
  21. data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -0
  22. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  23. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  24. data/lib/ruby_llm/agents/base.rb +15 -805
  25. data/lib/ruby_llm/agents/version.rb +1 -1
  26. metadata +12 -20
  27. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  28. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  29. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  30. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  31. 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
 
@@ -65,6 +52,7 @@
65
52
  <p class="text-sm text-gray-500 dark:text-gray-400">
66
53
  <%= number_with_delimiter(@stats[:count]) %> total executions
67
54
  </p>
55
+
68
56
  <div class="flex items-center justify-end gap-3 mt-1">
69
57
  <% status_colors = {
70
58
  "success" => "bg-green-500",
@@ -72,9 +60,11 @@
72
60
  "timeout" => "bg-yellow-500",
73
61
  "running" => "bg-blue-500"
74
62
  } %>
63
+
75
64
  <% @status_distribution.each do |status, count| %>
76
65
  <div class="flex items-center gap-1">
77
66
  <span class="w-2 h-2 rounded-full <%= status_colors[status] || 'bg-gray-400' %> <%= status == 'running' ? 'animate-pulse' : '' %>"></span>
67
+
78
68
  <span class="text-xs text-gray-600 dark:text-gray-400">
79
69
  <%= number_with_delimiter(count) %>
80
70
  </span>
@@ -89,8 +79,18 @@
89
79
  <% if @config && @circuit_breaker_status.present? %>
90
80
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
91
81
  <div class="flex items-center justify-between mb-3">
92
- <h3 class="text-sm font-medium text-gray-700 dark:text-gray-300 uppercase tracking-wider">Circuit Breaker Status</h3>
93
- <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>
94
94
  </div>
95
95
 
96
96
  <div class="flex flex-wrap gap-3">
@@ -99,11 +99,21 @@
99
99
  <% if status[:open] %>
100
100
  <!-- Open/Tripped indicator -->
101
101
  <span class="relative flex h-3 w-3">
102
- <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
+
103
109
  <span class="relative inline-flex rounded-full h-3 w-3 bg-red-500"></span>
104
110
  </span>
105
- <span class="text-sm font-medium text-red-700 dark:text-red-300"><%= model_id %></span>
106
- <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>
107
117
  <% if status[:cooldown_remaining] %>
108
118
  <span class="text-xs text-red-400 dark:text-red-500 ml-1">
109
119
  (resets in <%= status[:cooldown_remaining] %>s)
@@ -112,11 +122,16 @@
112
122
  <% else %>
113
123
  <!-- Closed/Healthy indicator -->
114
124
  <span class="inline-flex rounded-full h-3 w-3 bg-green-500"></span>
115
- <span class="text-sm font-medium text-green-700 dark:text-green-300"><%= model_id %></span>
116
- <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>
117
131
  <% if status[:failure_count] && status[:failure_count] > 0 %>
118
132
  <span class="text-xs text-yellow-500 dark:text-yellow-400 ml-1">
119
- (<%= status[:failure_count] %>/<%= status[:threshold] %> failures)
133
+ (<%= status[:failure_count] %>/<%= status[:threshold] %>
134
+ failures)
120
135
  </span>
121
136
  <% end %>
122
137
  <% end %>
@@ -126,8 +141,8 @@
126
141
 
127
142
  <% if @circuit_breaker_status.values.any? { |s| s[:open] } %>
128
143
  <p class="mt-3 text-xs text-gray-500 dark:text-gray-400">
129
- Open circuit breakers will skip the model and try fallbacks (if configured).
130
- 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.
131
146
  </p>
132
147
  <% end %>
133
148
  </div>
@@ -136,6 +151,7 @@
136
151
  <!-- Stats Grid -->
137
152
  <% success_rate = @stats[:success_rate] || 0 %>
138
153
  <% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
154
+
139
155
  <div class="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
140
156
  <%= render "rubyllm/agents/shared/stat_card",
141
157
  title: "Executions",
@@ -182,46 +198,60 @@
182
198
  <!-- Charts Section -->
183
199
  <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
184
200
  <!-- Executions Over Time -->
185
- <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">
186
202
  <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
187
203
  Executions (30 days)
188
204
  </h3>
189
-
190
- <div id="executions-chart" style="height: 250px;">
191
- <% success_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:count] - (d[:error_count] || 0)] }.to_h
192
- failed_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:error_count] || 0] }.to_h %>
193
-
194
- <%= area_chart [
195
- { name: "Success", data: success_data },
196
- { name: "Failed", data: failed_data }
197
- ], colors: ["#10B981", "#EF4444"], stacked: true, library: {
198
- yAxis: { min: 0 },
199
- legend: { align: "center", verticalAlign: "bottom" },
200
- plotOptions: { area: { stacking: "normal" } }
201
- } %>
202
- </div>
205
+ <div id="executions-chart" style="height: 220px;"></div>
203
206
  </div>
204
207
 
205
208
  <!-- Cost Over Time -->
206
- <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 pb-8 overflow-hidden">
207
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">Cost (30 days)</h3>
208
-
209
- <div id="cost-chart" style="height: 250px;">
210
- <% cost_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:total_cost].to_f.round(4)] }.to_h %>
211
-
212
- <%= line_chart cost_data, colors: ["#10B981"], prefix: "$", library: {
213
- yAxis: { min: 0 },
214
- legend: { enabled: false }
215
- } %>
216
- </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>
217
214
  </div>
218
215
  </div>
219
216
 
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
+ });
234
+
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>
247
+
220
248
  <!-- Finish Reason Distribution -->
221
249
  <% if @finish_reason_distribution.present? && @finish_reason_distribution.any? %>
222
250
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-4 mb-6">
223
251
  <div class="flex items-center justify-between">
224
- <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>
225
255
 
226
256
  <div class="flex flex-wrap gap-4">
227
257
  <% finish_colors = {
@@ -239,7 +269,9 @@
239
269
  style="background-color: <%= finish_colors[reason] || '#6B7280' %>"
240
270
  ></span>
241
271
 
242
- <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>
243
275
 
244
276
  <span class="text-sm font-medium text-gray-900 dark:text-gray-100 ml-1">
245
277
  (<%= number_with_delimiter(count) %>)
@@ -258,31 +290,48 @@
258
290
  <% if @config %>
259
291
  <!-- Configuration -->
260
292
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
261
- <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>
262
296
 
263
297
  <!-- Basic Configuration -->
264
- <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
+
265
302
  <div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
266
303
  <div>
267
304
  <p class="text-sm text-gray-500 dark:text-gray-400">Model</p>
268
- <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>
269
309
  </div>
270
310
 
271
311
  <div>
272
312
  <p class="text-sm text-gray-500 dark:text-gray-400">Temperature</p>
273
- <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>
274
317
  </div>
275
318
 
276
319
  <div>
277
320
  <p class="text-sm text-gray-500 dark:text-gray-400">Timeout</p>
278
- <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>
279
325
  </div>
280
326
 
281
327
  <div>
282
328
  <p class="text-sm text-gray-500 dark:text-gray-400">Cache</p>
329
+
283
330
  <p class="font-medium text-gray-900 dark:text-gray-100">
284
331
  <% if @config[:cache_enabled] %>
285
- Enabled (<%= @config[:cache_ttl].inspect %>)
332
+ Enabled (
333
+ <%= @config[:cache_ttl].inspect %>
334
+ )
286
335
  <% else %>
287
336
  <span class="text-gray-400 dark:text-gray-500">Disabled</span>
288
337
  <% end %>
@@ -291,16 +340,22 @@
291
340
  </div>
292
341
 
293
342
  <!-- Reliability Configuration -->
294
- <%
295
- retries_config = @config[:retries] || {}
343
+ <% retries_config = @config[:retries] || {}
296
344
  has_retries = (retries_config[:max] || 0) > 0
297
345
  has_fallbacks = @config[:fallback_models].present? && @config[:fallback_models].any?
298
346
  has_total_timeout = @config[:total_timeout].present?
299
347
  has_circuit_breaker = @config[:circuit_breaker].present?
300
- has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker
301
- %>
348
+ has_any_reliability = has_retries || has_fallbacks || has_total_timeout || has_circuit_breaker %>
349
+
302
350
  <div class="border-t border-gray-100 dark:border-gray-700 pt-4 mb-6">
303
- <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>
304
359
 
305
360
  <% if has_any_reliability %>
306
361
  <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
@@ -308,26 +363,48 @@
308
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' %>">
309
364
  <div class="flex-shrink-0 mt-0.5">
310
365
  <% if has_retries %>
311
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
312
- <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
+ />
313
376
  </svg>
314
377
  <% else %>
315
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
316
- <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
+ />
317
388
  </svg>
318
389
  <% end %>
319
390
  </div>
391
+
320
392
  <div>
321
- <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
+
322
397
  <% if has_retries %>
323
398
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
324
- Max: <%= retries_config[:max] %> &middot;
325
- Backoff: <%= retries_config[:backoff] %> &middot;
326
- Base: <%= retries_config[:base] %>s &middot;
327
- 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
328
403
  </p>
329
404
  <% else %>
330
- <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>
331
408
  <% end %>
332
409
  </div>
333
410
  </div>
@@ -336,23 +413,45 @@
336
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' %>">
337
414
  <div class="flex-shrink-0 mt-0.5">
338
415
  <% if has_fallbacks %>
339
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
340
- <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
+ />
341
426
  </svg>
342
427
  <% else %>
343
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
344
- <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
+ />
345
438
  </svg>
346
439
  <% end %>
347
440
  </div>
441
+
348
442
  <div>
349
- <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
+
350
447
  <% if has_fallbacks %>
351
448
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
352
449
  <%= @config[:fallback_models].join(" → ") %>
353
450
  </p>
354
451
  <% else %>
355
- <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>
356
455
  <% end %>
357
456
  </div>
358
457
  </div>
@@ -361,23 +460,45 @@
361
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' %>">
362
461
  <div class="flex-shrink-0 mt-0.5">
363
462
  <% if has_total_timeout %>
364
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
365
- <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
+ />
366
473
  </svg>
367
474
  <% else %>
368
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
369
- <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
+ />
370
485
  </svg>
371
486
  <% end %>
372
487
  </div>
488
+
373
489
  <div>
374
- <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
+
375
494
  <% if has_total_timeout %>
376
495
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
377
496
  <%= @config[:total_timeout] %> seconds across all attempts
378
497
  </p>
379
498
  <% else %>
380
- <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>
381
502
  <% end %>
382
503
  </div>
383
504
  </div>
@@ -386,43 +507,79 @@
386
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' %>">
387
508
  <div class="flex-shrink-0 mt-0.5">
388
509
  <% if has_circuit_breaker %>
389
- <svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20">
390
- <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
+ />
391
520
  </svg>
392
521
  <% else %>
393
- <svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
394
- <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
+ />
395
532
  </svg>
396
533
  <% end %>
397
534
  </div>
535
+
398
536
  <div>
399
- <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
+
400
541
  <% if has_circuit_breaker %>
401
542
  <% cb = @config[:circuit_breaker] %>
402
543
  <p class="text-xs text-gray-600 dark:text-gray-400 mt-1">
403
- Opens after <%= cb[:errors] %> errors within <%= cb[:within] %>s &middot;
404
- Cooldown: <%= cb[:cooldown] %>s
544
+ Opens after <%= cb[:errors] %> errors within
545
+ <%= cb[:within] %>s &middot; Cooldown: <%= cb[:cooldown] %>s
405
546
  </p>
406
547
  <% else %>
407
- <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>
408
551
  <% end %>
409
552
  </div>
410
553
  </div>
411
554
  </div>
412
555
  <% else %>
413
- <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>
414
559
  <% end %>
415
560
  </div>
416
561
 
417
562
  <!-- Parameters -->
418
563
  <% if @config[:params].present? && @config[:params].any? %>
419
564
  <div class="border-t border-gray-100 dark:border-gray-700 pt-4">
420
- <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>
421
573
 
422
574
  <div class="space-y-2">
423
575
  <% @config[:params].each do |name, opts| %>
424
576
  <div class="flex items-center text-sm">
425
- <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
+ >
426
583
  <%= name %>
427
584
  </code>
428
585
 
@@ -435,7 +592,9 @@
435
592
  default: <%= opts[:default].inspect %>
436
593
  </span>
437
594
  <% else %>
438
- <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>
439
598
  <% end %>
440
599
  </div>
441
600
  <% end %>
@@ -447,7 +606,9 @@
447
606
 
448
607
  <!-- Executions -->
449
608
  <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
450
- <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>
451
612
 
452
613
  <%= turbo_frame_tag "executions_table" do %>
453
614
  <%
@@ -456,217 +617,84 @@
456
617
  selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
457
618
  selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
458
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
+ ]
459
636
  %>
460
637
 
461
- <%= 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| %>
462
639
  <div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100 dark:border-gray-700">
463
- <!-- Status Filter (Multi-select) -->
464
- <div class="relative filter-dropdown" data-filter="statuses">
465
- <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' : '' %>">
466
- <% if selected_statuses.length == 1
467
- status_color = case selected_statuses.first
468
- when 'success' then 'bg-green-500'
469
- when 'error' then 'bg-red-500'
470
- when 'running' then 'bg-blue-500'
471
- when 'timeout' then 'bg-yellow-500'
472
- else 'bg-gray-400'
473
- end
474
- else
475
- status_color = 'bg-gray-400'
476
- end %>
477
- <span class="w-2 h-2 rounded-full <%= status_color %>"></span>
478
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
479
- <% if selected_statuses.empty? %>
480
- All Statuses
481
- <% elsif selected_statuses.length == 1 %>
482
- <%= selected_statuses.first.capitalize %>
483
- <% else %>
484
- <%= selected_statuses.length %> Statuses
485
- <% end %>
486
- </span>
487
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
488
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
489
- </svg>
490
- </button>
491
- <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">
492
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
493
- <label class="flex items-center gap-2 cursor-pointer">
494
- <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' : '' %>>
495
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Statuses</span>
496
- </label>
497
- </div>
498
- <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">
499
- <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' : '' %>>
500
- <span class="w-2 h-2 rounded-full bg-green-500"></span>
501
- Success
502
- </label>
503
- <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">
504
- <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' : '' %>>
505
- <span class="w-2 h-2 rounded-full bg-red-500"></span>
506
- Error
507
- </label>
508
- <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">
509
- <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' : '' %>>
510
- <span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
511
- Running
512
- </label>
513
- <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">
514
- <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' : '' %>>
515
- <span class="w-2 h-2 rounded-full bg-yellow-500"></span>
516
- Timeout
517
- </label>
518
- </div>
519
- </div>
520
-
521
- <!-- 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) %>
522
650
  <% if @versions.any? %>
523
- <div class="relative filter-dropdown" data-filter="versions">
524
- <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' : '' %>">
525
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
526
- <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"/>
527
- </svg>
528
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
529
- <% if selected_versions.empty? %>
530
- All Versions
531
- <% elsif selected_versions.length == 1 %>
532
- v<%= selected_versions.first %>
533
- <% else %>
534
- <%= selected_versions.length %> Versions
535
- <% end %>
536
- </span>
537
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
538
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
539
- </svg>
540
- </button>
541
- <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">
542
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
543
- <label class="flex items-center gap-2 cursor-pointer">
544
- <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' : '' %>>
545
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Versions</span>
546
- </label>
547
- </div>
548
- <% @versions.each do |version| %>
549
- <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">
550
- <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' : '' %>>
551
- v<%= version %>
552
- </label>
553
- <% end %>
554
- </div>
555
- </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" %>
556
659
  <% end %>
557
660
 
558
- <!-- Model Filter (Multi-select) -->
661
+ <%# Model Filter (Multi-select) %>
559
662
  <% if @models.length > 1 %>
560
- <div class="relative filter-dropdown" data-filter="models">
561
- <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' : '' %>">
562
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
563
- <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"/>
564
- </svg>
565
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
566
- <% if selected_models.empty? %>
567
- All Models
568
- <% elsif selected_models.length == 1 %>
569
- <%= selected_models.first %>
570
- <% else %>
571
- <%= selected_models.length %> Models
572
- <% end %>
573
- </span>
574
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
575
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
576
- </svg>
577
- </button>
578
- <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">
579
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
580
- <label class="flex items-center gap-2 cursor-pointer">
581
- <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' : '' %>>
582
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Models</span>
583
- </label>
584
- </div>
585
- <% @models.each do |model| %>
586
- <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">
587
- <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' : '' %>>
588
- <%= model %>
589
- </label>
590
- <% end %>
591
- </div>
592
- </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" %>
593
671
  <% end %>
594
672
 
595
- <!-- Temperature Filter (Multi-select) -->
673
+ <%# Temperature Filter (Multi-select) %>
596
674
  <% if @temperatures.length > 1 %>
597
- <div class="relative filter-dropdown" data-filter="temperatures">
598
- <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' : '' %>">
599
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
600
- <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"/>
601
- </svg>
602
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
603
- <% if selected_temperatures.empty? %>
604
- All Temps
605
- <% elsif selected_temperatures.length == 1 %>
606
- <%= selected_temperatures.first %>
607
- <% else %>
608
- <%= selected_temperatures.length %> Temps
609
- <% end %>
610
- </span>
611
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
612
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
613
- </svg>
614
- </button>
615
- <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">
616
- <div class="px-3 py-2 border-b border-gray-100 dark:border-gray-600">
617
- <label class="flex items-center gap-2 cursor-pointer">
618
- <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' : '' %>>
619
- <span class="text-sm font-medium text-gray-700 dark:text-gray-200">All Temps</span>
620
- </label>
621
- </div>
622
- <% @temperatures.each do |temp| %>
623
- <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">
624
- <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' : '' %>>
625
- <%= temp %>
626
- </label>
627
- <% end %>
628
- </div>
629
- </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" %>
630
683
  <% end %>
631
684
 
632
- <!-- Time Range Filter -->
633
- <div class="relative filter-dropdown" data-filter="days">
634
- <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' : '' %>">
635
- <svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
636
- <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"/>
637
- </svg>
638
- <span class="dropdown-label text-gray-700 dark:text-gray-200">
639
- <% case params[:days]
640
- when '1' then %>Today<%
641
- when '7' then %>Last 7 Days<%
642
- when '30' then %>Last 30 Days<%
643
- else %>All Time<%
644
- end %>
645
- </span>
646
- <svg class="w-4 h-4 text-gray-400 dark:text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
647
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
648
- </svg>
649
- </button>
650
- <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">
651
- <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' : '' %>">
652
- All Time
653
- </a>
654
- <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' : '' %>">
655
- Today
656
- </a>
657
- <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' : '' %>">
658
- Last 7 Days
659
- </a>
660
- <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' : '' %>">
661
- Last 30 Days
662
- </a>
663
- </div>
664
- <%= f.hidden_field :days, value: params[:days], id: "filter_days" %>
665
- </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" %>
666
692
 
667
- <!-- Clear Filters -->
693
+ <%# Clear Filters %>
668
694
  <% if has_filters %>
669
- <%= 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 %>
670
698
  <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
671
699
  <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
672
700
  </svg>
@@ -674,7 +702,7 @@
674
702
  <% end %>
675
703
  <% end %>
676
704
 
677
- <!-- Stats Summary (right aligned) -->
705
+ <%# Stats Summary (right aligned) %>
678
706
  <div class="ml-auto flex items-center gap-4 text-sm text-gray-500 dark:text-gray-400">
679
707
  <span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
680
708
  <span class="text-gray-300 dark:text-gray-600">|</span>
@@ -688,85 +716,3 @@
688
716
  <%= render "rubyllm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
689
717
  <% end %>
690
718
  </div>
691
-
692
- <script>
693
- function toggleDropdown(button) {
694
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
695
- if (menu !== button.nextElementSibling) {
696
- menu.classList.add('hidden');
697
- }
698
- });
699
- button.nextElementSibling.classList.toggle('hidden');
700
- }
701
-
702
- function selectSingleFilter(name, value, label) {
703
- document.getElementById('filter_' + name).value = value;
704
- const dropdown = document.querySelector(`[data-filter="${name}"]`);
705
- dropdown.querySelector('.dropdown-label').textContent = label;
706
- const button = dropdown.querySelector('button');
707
- if (value) {
708
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
709
- } else {
710
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
711
- }
712
- dropdown.querySelector('.dropdown-menu').classList.add('hidden');
713
- document.getElementById('agent-filters-form').requestSubmit();
714
- }
715
-
716
- function toggleAllOptions(checkbox, filterName) {
717
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
718
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox');
719
-
720
- if (checkbox.checked) {
721
- checkboxes.forEach(cb => cb.checked = false);
722
- updateMultiSelect(filterName);
723
- }
724
- }
725
-
726
- function updateMultiSelect(filterName) {
727
- const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
728
- const checkboxes = dropdown.querySelectorAll('.filter-checkbox:checked');
729
- const selectAllCheckbox = dropdown.querySelector('.select-all-checkbox');
730
- const button = dropdown.querySelector('button');
731
- const label = dropdown.querySelector('.dropdown-label');
732
-
733
- const count = checkboxes.length;
734
-
735
- selectAllCheckbox.checked = (count === 0);
736
-
737
- const labelMap = {
738
- 'statuses': { all: 'All Statuses', plural: 'Statuses' },
739
- 'versions': { all: 'All Versions', plural: 'Versions' },
740
- 'models': { all: 'All Models', plural: 'Models' },
741
- 'temperatures': { all: 'All Temps', plural: 'Temps' }
742
- };
743
-
744
- if (count === 0) {
745
- label.textContent = labelMap[filterName]?.all || 'All';
746
- button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
747
- } else if (count === 1) {
748
- const value = checkboxes[0].value;
749
- if (filterName === 'statuses') {
750
- label.textContent = value.charAt(0).toUpperCase() + value.slice(1);
751
- } else if (filterName === 'versions') {
752
- label.textContent = 'v' + value;
753
- } else {
754
- label.textContent = value;
755
- }
756
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
757
- } else {
758
- label.textContent = `${count} ${labelMap[filterName]?.plural || 'Items'}`;
759
- button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
760
- }
761
-
762
- document.getElementById('agent-filters-form').requestSubmit();
763
- }
764
-
765
- document.addEventListener('click', function(e) {
766
- if (!e.target.closest('.filter-dropdown')) {
767
- document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
768
- menu.classList.add('hidden');
769
- });
770
- }
771
- });
772
- </script>