ruby_llm-agents 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +898 -0
- data/app/channels/ruby_llm/agents/executions_channel.rb +23 -0
- data/app/controllers/ruby_llm/agents/agents_controller.rb +100 -0
- data/app/controllers/ruby_llm/agents/application_controller.rb +20 -0
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +34 -0
- data/app/controllers/ruby_llm/agents/executions_controller.rb +93 -0
- data/app/helpers/ruby_llm/agents/application_helper.rb +149 -0
- data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +56 -0
- data/app/javascript/ruby_llm/agents/controllers/index.js +12 -0
- data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +83 -0
- data/app/models/ruby_llm/agents/execution/analytics.rb +166 -0
- data/app/models/ruby_llm/agents/execution/metrics.rb +89 -0
- data/app/models/ruby_llm/agents/execution/scopes.rb +81 -0
- data/app/models/ruby_llm/agents/execution.rb +81 -0
- data/app/services/ruby_llm/agents/agent_registry.rb +112 -0
- data/app/views/layouts/rubyllm/agents/application.html.erb +276 -0
- data/app/views/rubyllm/agents/agents/index.html.erb +89 -0
- data/app/views/rubyllm/agents/agents/show.html.erb +562 -0
- data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +48 -0
- data/app/views/rubyllm/agents/dashboard/index.html.erb +121 -0
- data/app/views/rubyllm/agents/executions/_execution.html.erb +64 -0
- data/app/views/rubyllm/agents/executions/_filters.html.erb +172 -0
- data/app/views/rubyllm/agents/executions/_list.html.erb +229 -0
- data/app/views/rubyllm/agents/executions/index.html.erb +83 -0
- data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +4 -0
- data/app/views/rubyllm/agents/executions/show.html.erb +240 -0
- data/app/views/rubyllm/agents/shared/_executions_table.html.erb +193 -0
- data/app/views/rubyllm/agents/shared/_stat_card.html.erb +14 -0
- data/app/views/rubyllm/agents/shared/_status_badge.html.erb +65 -0
- data/app/views/rubyllm/agents/shared/_status_dot.html.erb +18 -0
- data/config/routes.rb +13 -0
- data/lib/generators/ruby_llm_agents/agent_generator.rb +79 -0
- data/lib/generators/ruby_llm_agents/install_generator.rb +89 -0
- data/lib/generators/ruby_llm_agents/templates/add_prompts_migration.rb.tt +12 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +46 -0
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +22 -0
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +36 -0
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +66 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +59 -0
- data/lib/ruby_llm/agents/base.rb +271 -0
- data/lib/ruby_llm/agents/configuration.rb +36 -0
- data/lib/ruby_llm/agents/engine.rb +32 -0
- data/lib/ruby_llm/agents/execution_logger_job.rb +59 -0
- data/lib/ruby_llm/agents/inflections.rb +13 -0
- data/lib/ruby_llm/agents/instrumentation.rb +245 -0
- data/lib/ruby_llm/agents/version.rb +7 -0
- data/lib/ruby_llm/agents.rb +26 -0
- data/lib/ruby_llm-agents.rb +3 -0
- metadata +164 -0
|
@@ -0,0 +1,562 @@
|
|
|
1
|
+
<div class="mb-6">
|
|
2
|
+
<%= link_to ruby_llm_agents.agents_path, class: "text-blue-600 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>
|
|
21
|
+
|
|
22
|
+
<!-- Header -->
|
|
23
|
+
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
24
|
+
<div class="flex items-start justify-between">
|
|
25
|
+
<div>
|
|
26
|
+
<div class="flex items-center space-x-3">
|
|
27
|
+
<h1 class="text-2xl font-bold text-gray-900">
|
|
28
|
+
<%= @agent_type.gsub(/Agent$/, '') %>
|
|
29
|
+
</h1>
|
|
30
|
+
|
|
31
|
+
<% if @agent_active %>
|
|
32
|
+
<span
|
|
33
|
+
class="
|
|
34
|
+
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
|
|
35
|
+
font-medium bg-green-100 text-green-800
|
|
36
|
+
"
|
|
37
|
+
>
|
|
38
|
+
Active
|
|
39
|
+
</span>
|
|
40
|
+
<% else %>
|
|
41
|
+
<span
|
|
42
|
+
class="
|
|
43
|
+
inline-flex items-center px-2.5 py-0.5 rounded-full text-xs
|
|
44
|
+
font-medium bg-gray-100 text-gray-600
|
|
45
|
+
"
|
|
46
|
+
>
|
|
47
|
+
Deleted
|
|
48
|
+
</span>
|
|
49
|
+
<% end %>
|
|
50
|
+
|
|
51
|
+
<% if @config %>
|
|
52
|
+
<span class="text-sm text-gray-500">v<%= @config[:version] %></span>
|
|
53
|
+
<% end %>
|
|
54
|
+
</div>
|
|
55
|
+
|
|
56
|
+
<% if @config %>
|
|
57
|
+
<p class="text-gray-500 mt-1">
|
|
58
|
+
<%= @config[:model] %> · temp <%= @config[:temperature] %>
|
|
59
|
+
· timeout <%= @config[:timeout] %>s
|
|
60
|
+
</p>
|
|
61
|
+
<% end %>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div class="text-right text-sm text-gray-500">
|
|
65
|
+
<p><%= @stats[:count] %> total executions</p>
|
|
66
|
+
</div>
|
|
67
|
+
</div>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<!-- Stats Grid -->
|
|
71
|
+
<% success_rate = @stats[:success_rate] || 0 %>
|
|
72
|
+
<% success_rate_color = success_rate >= 95 ? 'text-green-600' : success_rate >= 80 ? 'text-yellow-600' : 'text-red-600' %>
|
|
73
|
+
<div class="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
|
74
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
75
|
+
title: "Executions",
|
|
76
|
+
value: number_with_delimiter(@stats[:count]),
|
|
77
|
+
subtitle: "Today: #{@stats_today[:count]}",
|
|
78
|
+
icon: "M13 10V3L4 14h7v7l9-11h-7z",
|
|
79
|
+
icon_color: "text-blue-500" %>
|
|
80
|
+
|
|
81
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
82
|
+
title: "Success Rate",
|
|
83
|
+
value: "#{success_rate}%",
|
|
84
|
+
subtitle: "Error rate: #{@stats[:error_rate] || 0}%",
|
|
85
|
+
icon: "M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
86
|
+
icon_color: "text-green-500",
|
|
87
|
+
value_color: success_rate_color %>
|
|
88
|
+
|
|
89
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
90
|
+
title: "Total Cost",
|
|
91
|
+
value: "$#{number_with_precision(@stats[:total_cost] || 0, precision: 4)}",
|
|
92
|
+
subtitle: "Avg: $#{number_with_precision(@stats[:avg_cost] || 0, precision: 6)}",
|
|
93
|
+
icon: "M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
94
|
+
icon_color: "text-amber-500" %>
|
|
95
|
+
|
|
96
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
97
|
+
title: "Total Tokens",
|
|
98
|
+
value: number_with_delimiter(@stats[:total_tokens] || 0),
|
|
99
|
+
subtitle: "Avg: #{number_with_delimiter(@stats[:avg_tokens] || 0)}",
|
|
100
|
+
icon: "M7 20l4-16m2 16l4-16M6 9h14M4 15h14",
|
|
101
|
+
icon_color: "text-indigo-500" %>
|
|
102
|
+
|
|
103
|
+
<%= render "rubyllm/agents/shared/stat_card",
|
|
104
|
+
title: "Avg Duration",
|
|
105
|
+
value: "#{number_with_delimiter(@stats[:avg_duration_ms] || 0)} ms",
|
|
106
|
+
icon: "M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z",
|
|
107
|
+
icon_color: "text-purple-500" %>
|
|
108
|
+
</div>
|
|
109
|
+
|
|
110
|
+
<!-- Charts Section -->
|
|
111
|
+
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
|
|
112
|
+
<!-- Executions Over Time -->
|
|
113
|
+
<div class="bg-white rounded-lg shadow p-6 pb-20 overflow-hidden">
|
|
114
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">
|
|
115
|
+
Executions (30 days)
|
|
116
|
+
</h3>
|
|
117
|
+
|
|
118
|
+
<div id="executions-chart" style="height: 250px;">
|
|
119
|
+
<% success_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:count] - (d[:error_count] || 0)] }.to_h
|
|
120
|
+
failed_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:error_count] || 0] }.to_h %>
|
|
121
|
+
|
|
122
|
+
<%= area_chart [
|
|
123
|
+
{ name: "Success", data: success_data },
|
|
124
|
+
{ name: "Failed", data: failed_data }
|
|
125
|
+
], colors: ["#10B981", "#EF4444"], stacked: true, library: { maintainAspectRatio: false } %>
|
|
126
|
+
</div>
|
|
127
|
+
</div>
|
|
128
|
+
|
|
129
|
+
<!-- Cost Over Time -->
|
|
130
|
+
<div class="bg-white rounded-lg shadow p-6 pb-8 overflow-hidden">
|
|
131
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Cost (30 days)</h3>
|
|
132
|
+
|
|
133
|
+
<div id="cost-chart" style="height: 250px;">
|
|
134
|
+
<% cost_data = @trend_data.map { |d| [d[:date].strftime("%b %d"), d[:total_cost].to_f.round(4)] }.to_h %>
|
|
135
|
+
|
|
136
|
+
<%= line_chart cost_data, colors: ["#10B981"], prefix: "$", library: { maintainAspectRatio: false, plugins: { legend: { display: false } } } %>
|
|
137
|
+
</div>
|
|
138
|
+
</div>
|
|
139
|
+
</div>
|
|
140
|
+
|
|
141
|
+
<!-- Status Distribution (compact) -->
|
|
142
|
+
<div class="bg-white rounded-lg shadow p-4 mb-6">
|
|
143
|
+
<div class="flex items-center justify-between">
|
|
144
|
+
<p class="text-sm text-gray-500 uppercase">Status Distribution</p>
|
|
145
|
+
|
|
146
|
+
<div class="flex flex-wrap gap-4">
|
|
147
|
+
<% status_colors = {
|
|
148
|
+
"success" => "#10B981",
|
|
149
|
+
"error" => "#EF4444",
|
|
150
|
+
"timeout" => "#F59E0B",
|
|
151
|
+
"running" => "#3B82F6"
|
|
152
|
+
} %>
|
|
153
|
+
|
|
154
|
+
<% @status_distribution.each do |status, count| %>
|
|
155
|
+
<div class="flex items-center">
|
|
156
|
+
<span
|
|
157
|
+
class="w-2 h-2 rounded-full mr-1.5"
|
|
158
|
+
style="background-color: <%= status_colors[status] || '#6B7280' %>"
|
|
159
|
+
></span>
|
|
160
|
+
|
|
161
|
+
<span class="text-sm text-gray-700 capitalize"><%= status %></span>
|
|
162
|
+
|
|
163
|
+
<span class="text-sm font-medium text-gray-900 ml-1">
|
|
164
|
+
(<%= number_with_delimiter(count) %>)
|
|
165
|
+
</span>
|
|
166
|
+
</div>
|
|
167
|
+
<% end %>
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
</div>
|
|
171
|
+
|
|
172
|
+
<% if @config %>
|
|
173
|
+
<!-- Configuration -->
|
|
174
|
+
<div class="bg-white rounded-lg shadow p-6 mb-6">
|
|
175
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Configuration</h3>
|
|
176
|
+
|
|
177
|
+
<div class="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
|
|
178
|
+
<div>
|
|
179
|
+
<p class="text-sm text-gray-500">Model</p>
|
|
180
|
+
<p class="font-medium"><%= @config[:model] %></p>
|
|
181
|
+
</div>
|
|
182
|
+
|
|
183
|
+
<div>
|
|
184
|
+
<p class="text-sm text-gray-500">Temperature</p>
|
|
185
|
+
<p class="font-medium"><%= @config[:temperature] %></p>
|
|
186
|
+
</div>
|
|
187
|
+
|
|
188
|
+
<div>
|
|
189
|
+
<p class="text-sm text-gray-500">Timeout</p>
|
|
190
|
+
<p class="font-medium"><%= @config[:timeout] %> seconds</p>
|
|
191
|
+
</div>
|
|
192
|
+
|
|
193
|
+
<div>
|
|
194
|
+
<p class="text-sm text-gray-500">Cache</p>
|
|
195
|
+
|
|
196
|
+
<p class="font-medium">
|
|
197
|
+
<% if @config[:cache_enabled] %>
|
|
198
|
+
Enabled (
|
|
199
|
+
<%= @config[:cache_ttl].inspect %>
|
|
200
|
+
)
|
|
201
|
+
<% else %>
|
|
202
|
+
Disabled
|
|
203
|
+
<% end %>
|
|
204
|
+
</p>
|
|
205
|
+
</div>
|
|
206
|
+
</div>
|
|
207
|
+
|
|
208
|
+
<% if @config[:params].present? && @config[:params].any? %>
|
|
209
|
+
<div class="border-t border-gray-100 pt-4">
|
|
210
|
+
<p class="text-sm text-gray-500 mb-2">Parameters</p>
|
|
211
|
+
|
|
212
|
+
<div class="space-y-2">
|
|
213
|
+
<% @config[:params].each do |name, opts| %>
|
|
214
|
+
<div class="flex items-center text-sm">
|
|
215
|
+
<code class="bg-gray-100 px-2 py-0.5 rounded font-mono">
|
|
216
|
+
<%= name %>
|
|
217
|
+
</code>
|
|
218
|
+
|
|
219
|
+
<% if opts[:required] %>
|
|
220
|
+
<span class="ml-2 text-xs text-red-500 font-medium">
|
|
221
|
+
required
|
|
222
|
+
</span>
|
|
223
|
+
<% elsif opts[:default].present? %>
|
|
224
|
+
<span class="ml-2 text-xs text-gray-400">
|
|
225
|
+
default: <%= opts[:default].inspect %>
|
|
226
|
+
</span>
|
|
227
|
+
<% else %>
|
|
228
|
+
<span class="ml-2 text-xs text-gray-400">optional</span>
|
|
229
|
+
<% end %>
|
|
230
|
+
</div>
|
|
231
|
+
<% end %>
|
|
232
|
+
</div>
|
|
233
|
+
</div>
|
|
234
|
+
<% end %>
|
|
235
|
+
</div>
|
|
236
|
+
<% end %>
|
|
237
|
+
|
|
238
|
+
<!-- Executions -->
|
|
239
|
+
<div class="bg-white rounded-lg shadow p-6">
|
|
240
|
+
<h3 class="text-lg font-semibold text-gray-900 mb-4">Executions</h3>
|
|
241
|
+
|
|
242
|
+
<%= turbo_frame_tag "executions_table" do %>
|
|
243
|
+
<%
|
|
244
|
+
has_filters = params[:statuses].present? || params[:versions].present? || params[:models].present? || params[:temperatures].present? || params[:days].present?
|
|
245
|
+
selected_statuses = params[:statuses].present? ? (params[:statuses].is_a?(Array) ? params[:statuses] : params[:statuses].split(",")) : []
|
|
246
|
+
selected_versions = params[:versions].present? ? (params[:versions].is_a?(Array) ? params[:versions] : params[:versions].split(",")) : []
|
|
247
|
+
selected_models = params[:models].present? ? (params[:models].is_a?(Array) ? params[:models] : params[:models].split(",")) : []
|
|
248
|
+
selected_temperatures = params[:temperatures].present? ? (params[:temperatures].is_a?(Array) ? params[:temperatures] : params[:temperatures].split(",")).map(&:to_s) : []
|
|
249
|
+
%>
|
|
250
|
+
|
|
251
|
+
<%= form_with url: ruby_llm_agents.agent_path(@agent_type), method: :get, data: { turbo_frame: "executions_table" }, id: "agent-filters-form" do |f| %>
|
|
252
|
+
<div class="flex flex-wrap items-center gap-3 mb-4 pb-4 border-b border-gray-100">
|
|
253
|
+
<!-- Status Filter (Multi-select) -->
|
|
254
|
+
<div class="relative filter-dropdown" data-filter="statuses">
|
|
255
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_statuses.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
256
|
+
<% if selected_statuses.length == 1
|
|
257
|
+
status_color = case selected_statuses.first
|
|
258
|
+
when 'success' then 'bg-green-500'
|
|
259
|
+
when 'error' then 'bg-red-500'
|
|
260
|
+
when 'running' then 'bg-blue-500'
|
|
261
|
+
when 'timeout' then 'bg-yellow-500'
|
|
262
|
+
else 'bg-gray-400'
|
|
263
|
+
end
|
|
264
|
+
else
|
|
265
|
+
status_color = 'bg-gray-400'
|
|
266
|
+
end %>
|
|
267
|
+
<span class="w-2 h-2 rounded-full <%= status_color %>"></span>
|
|
268
|
+
<span class="dropdown-label text-gray-700">
|
|
269
|
+
<% if selected_statuses.empty? %>
|
|
270
|
+
All Statuses
|
|
271
|
+
<% elsif selected_statuses.length == 1 %>
|
|
272
|
+
<%= selected_statuses.first.capitalize %>
|
|
273
|
+
<% else %>
|
|
274
|
+
<%= selected_statuses.length %> Statuses
|
|
275
|
+
<% end %>
|
|
276
|
+
</span>
|
|
277
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
278
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
279
|
+
</svg>
|
|
280
|
+
</button>
|
|
281
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
|
|
282
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
283
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
284
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'statuses')" <%= selected_statuses.empty? ? 'checked' : '' %>>
|
|
285
|
+
<span class="text-sm font-medium text-gray-700">All Statuses</span>
|
|
286
|
+
</label>
|
|
287
|
+
</div>
|
|
288
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
289
|
+
<input type="checkbox" name="statuses[]" value="success" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('success') ? 'checked' : '' %>>
|
|
290
|
+
<span class="w-2 h-2 rounded-full bg-green-500"></span>
|
|
291
|
+
Success
|
|
292
|
+
</label>
|
|
293
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
294
|
+
<input type="checkbox" name="statuses[]" value="error" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('error') ? 'checked' : '' %>>
|
|
295
|
+
<span class="w-2 h-2 rounded-full bg-red-500"></span>
|
|
296
|
+
Error
|
|
297
|
+
</label>
|
|
298
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
299
|
+
<input type="checkbox" name="statuses[]" value="running" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('running') ? 'checked' : '' %>>
|
|
300
|
+
<span class="w-2 h-2 rounded-full bg-blue-500 animate-pulse"></span>
|
|
301
|
+
Running
|
|
302
|
+
</label>
|
|
303
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
304
|
+
<input type="checkbox" name="statuses[]" value="timeout" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('statuses')" <%= selected_statuses.include?('timeout') ? 'checked' : '' %>>
|
|
305
|
+
<span class="w-2 h-2 rounded-full bg-yellow-500"></span>
|
|
306
|
+
Timeout
|
|
307
|
+
</label>
|
|
308
|
+
</div>
|
|
309
|
+
</div>
|
|
310
|
+
|
|
311
|
+
<!-- Version Filter (Multi-select) -->
|
|
312
|
+
<% if @versions.any? %>
|
|
313
|
+
<div class="relative filter-dropdown" data-filter="versions">
|
|
314
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_versions.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
315
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
316
|
+
<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"/>
|
|
317
|
+
</svg>
|
|
318
|
+
<span class="dropdown-label text-gray-700">
|
|
319
|
+
<% if selected_versions.empty? %>
|
|
320
|
+
All Versions
|
|
321
|
+
<% elsif selected_versions.length == 1 %>
|
|
322
|
+
v<%= selected_versions.first %>
|
|
323
|
+
<% else %>
|
|
324
|
+
<%= selected_versions.length %> Versions
|
|
325
|
+
<% end %>
|
|
326
|
+
</span>
|
|
327
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
328
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
329
|
+
</svg>
|
|
330
|
+
</button>
|
|
331
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-44 bg-white rounded-lg shadow-lg border border-gray-200 py-1 max-h-64 overflow-y-auto">
|
|
332
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
333
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
334
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'versions')" <%= selected_versions.empty? ? 'checked' : '' %>>
|
|
335
|
+
<span class="text-sm font-medium text-gray-700">All Versions</span>
|
|
336
|
+
</label>
|
|
337
|
+
</div>
|
|
338
|
+
<% @versions.each do |version| %>
|
|
339
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
340
|
+
<input type="checkbox" name="versions[]" value="<%= version %>" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('versions')" <%= selected_versions.include?(version.to_s) ? 'checked' : '' %>>
|
|
341
|
+
v<%= version %>
|
|
342
|
+
</label>
|
|
343
|
+
<% end %>
|
|
344
|
+
</div>
|
|
345
|
+
</div>
|
|
346
|
+
<% end %>
|
|
347
|
+
|
|
348
|
+
<!-- Model Filter (Multi-select) -->
|
|
349
|
+
<% if @models.length > 1 %>
|
|
350
|
+
<div class="relative filter-dropdown" data-filter="models">
|
|
351
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_models.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
352
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
353
|
+
<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"/>
|
|
354
|
+
</svg>
|
|
355
|
+
<span class="dropdown-label text-gray-700">
|
|
356
|
+
<% if selected_models.empty? %>
|
|
357
|
+
All Models
|
|
358
|
+
<% elsif selected_models.length == 1 %>
|
|
359
|
+
<%= selected_models.first %>
|
|
360
|
+
<% else %>
|
|
361
|
+
<%= selected_models.length %> Models
|
|
362
|
+
<% end %>
|
|
363
|
+
</span>
|
|
364
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
365
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
366
|
+
</svg>
|
|
367
|
+
</button>
|
|
368
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-56 bg-white rounded-lg shadow-lg border border-gray-200 py-1 max-h-64 overflow-y-auto">
|
|
369
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
370
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
371
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'models')" <%= selected_models.empty? ? 'checked' : '' %>>
|
|
372
|
+
<span class="text-sm font-medium text-gray-700">All Models</span>
|
|
373
|
+
</label>
|
|
374
|
+
</div>
|
|
375
|
+
<% @models.each do |model| %>
|
|
376
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
377
|
+
<input type="checkbox" name="models[]" value="<%= model %>" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('models')" <%= selected_models.include?(model) ? 'checked' : '' %>>
|
|
378
|
+
<%= model %>
|
|
379
|
+
</label>
|
|
380
|
+
<% end %>
|
|
381
|
+
</div>
|
|
382
|
+
</div>
|
|
383
|
+
<% end %>
|
|
384
|
+
|
|
385
|
+
<!-- Temperature Filter (Multi-select) -->
|
|
386
|
+
<% if @temperatures.length > 1 %>
|
|
387
|
+
<div class="relative filter-dropdown" data-filter="temperatures">
|
|
388
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= selected_temperatures.any? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
389
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
390
|
+
<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"/>
|
|
391
|
+
</svg>
|
|
392
|
+
<span class="dropdown-label text-gray-700">
|
|
393
|
+
<% if selected_temperatures.empty? %>
|
|
394
|
+
All Temps
|
|
395
|
+
<% elsif selected_temperatures.length == 1 %>
|
|
396
|
+
<%= selected_temperatures.first %>
|
|
397
|
+
<% else %>
|
|
398
|
+
<%= selected_temperatures.length %> Temps
|
|
399
|
+
<% end %>
|
|
400
|
+
</span>
|
|
401
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
402
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
403
|
+
</svg>
|
|
404
|
+
</button>
|
|
405
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-36 bg-white rounded-lg shadow-lg border border-gray-200 py-1 max-h-64 overflow-y-auto">
|
|
406
|
+
<div class="px-3 py-2 border-b border-gray-100">
|
|
407
|
+
<label class="flex items-center gap-2 cursor-pointer">
|
|
408
|
+
<input type="checkbox" class="select-all-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="toggleAllOptions(this, 'temperatures')" <%= selected_temperatures.empty? ? 'checked' : '' %>>
|
|
409
|
+
<span class="text-sm font-medium text-gray-700">All Temps</span>
|
|
410
|
+
</label>
|
|
411
|
+
</div>
|
|
412
|
+
<% @temperatures.each do |temp| %>
|
|
413
|
+
<label class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 cursor-pointer">
|
|
414
|
+
<input type="checkbox" name="temperatures[]" value="<%= temp %>" class="filter-checkbox rounded border-gray-300 text-blue-600 focus:ring-blue-500" onchange="updateMultiSelect('temperatures')" <%= selected_temperatures.include?(temp.to_s) ? 'checked' : '' %>>
|
|
415
|
+
<%= temp %>
|
|
416
|
+
</label>
|
|
417
|
+
<% end %>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
<% end %>
|
|
421
|
+
|
|
422
|
+
<!-- Time Range Filter -->
|
|
423
|
+
<div class="relative filter-dropdown" data-filter="days">
|
|
424
|
+
<button type="button" onclick="toggleDropdown(this)" class="flex items-center gap-2 px-3 py-2 text-sm bg-gray-50 border border-gray-200 rounded-lg hover:bg-gray-100 transition-colors <%= params[:days].present? ? 'ring-2 ring-blue-500 ring-offset-1' : '' %>">
|
|
425
|
+
<svg class="w-4 h-4 text-gray-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
426
|
+
<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"/>
|
|
427
|
+
</svg>
|
|
428
|
+
<span class="dropdown-label text-gray-700">
|
|
429
|
+
<% case params[:days]
|
|
430
|
+
when '1' then %>Today<%
|
|
431
|
+
when '7' then %>Last 7 Days<%
|
|
432
|
+
when '30' then %>Last 30 Days<%
|
|
433
|
+
else %>All Time<%
|
|
434
|
+
end %>
|
|
435
|
+
</span>
|
|
436
|
+
<svg class="w-4 h-4 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
437
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
|
|
438
|
+
</svg>
|
|
439
|
+
</button>
|
|
440
|
+
<div class="dropdown-menu hidden absolute z-10 mt-1 w-40 bg-white rounded-lg shadow-lg border border-gray-200 py-1">
|
|
441
|
+
<a href="#" onclick="selectSingleFilter('days', '', 'All Time'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days].blank? ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
442
|
+
All Time
|
|
443
|
+
</a>
|
|
444
|
+
<a href="#" onclick="selectSingleFilter('days', '1', 'Today'); return false;" class="flex items-center gap-2 px-3 py-2 text-sm text-gray-700 hover:bg-gray-50 <%= params[:days] == '1' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
445
|
+
Today
|
|
446
|
+
</a>
|
|
447
|
+
<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 hover:bg-gray-50 <%= params[:days] == '7' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
448
|
+
Last 7 Days
|
|
449
|
+
</a>
|
|
450
|
+
<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 hover:bg-gray-50 <%= params[:days] == '30' ? 'bg-blue-50 text-blue-700' : '' %>">
|
|
451
|
+
Last 30 Days
|
|
452
|
+
</a>
|
|
453
|
+
</div>
|
|
454
|
+
<%= f.hidden_field :days, value: params[:days], id: "filter_days" %>
|
|
455
|
+
</div>
|
|
456
|
+
|
|
457
|
+
<!-- Clear Filters -->
|
|
458
|
+
<% if has_filters %>
|
|
459
|
+
<%= 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 hover:text-red-600 hover:bg-red-50 rounded-lg transition-colors" do %>
|
|
460
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
461
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
|
462
|
+
</svg>
|
|
463
|
+
Clear
|
|
464
|
+
<% end %>
|
|
465
|
+
<% end %>
|
|
466
|
+
|
|
467
|
+
<!-- Stats Summary (right aligned) -->
|
|
468
|
+
<div class="ml-auto flex items-center gap-4 text-sm text-gray-500">
|
|
469
|
+
<span><%= number_with_delimiter(@filter_stats[:total_count]) %> executions</span>
|
|
470
|
+
<span class="text-gray-300">|</span>
|
|
471
|
+
<span>$<%= number_with_precision(@filter_stats[:total_cost] || 0, precision: 4) %></span>
|
|
472
|
+
<span class="text-gray-300">|</span>
|
|
473
|
+
<span><%= number_with_delimiter(@filter_stats[:total_tokens] || 0) %> tokens</span>
|
|
474
|
+
</div>
|
|
475
|
+
</div>
|
|
476
|
+
<% end %>
|
|
477
|
+
|
|
478
|
+
<%= render "rubyllm/agents/shared/executions_table", executions: @executions, pagination: @pagination %>
|
|
479
|
+
<% end %>
|
|
480
|
+
</div>
|
|
481
|
+
|
|
482
|
+
<script>
|
|
483
|
+
function toggleDropdown(button) {
|
|
484
|
+
document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
|
|
485
|
+
if (menu !== button.nextElementSibling) {
|
|
486
|
+
menu.classList.add('hidden');
|
|
487
|
+
}
|
|
488
|
+
});
|
|
489
|
+
button.nextElementSibling.classList.toggle('hidden');
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function selectSingleFilter(name, value, label) {
|
|
493
|
+
document.getElementById('filter_' + name).value = value;
|
|
494
|
+
const dropdown = document.querySelector(`[data-filter="${name}"]`);
|
|
495
|
+
dropdown.querySelector('.dropdown-label').textContent = label;
|
|
496
|
+
const button = dropdown.querySelector('button');
|
|
497
|
+
if (value) {
|
|
498
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
499
|
+
} else {
|
|
500
|
+
button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
501
|
+
}
|
|
502
|
+
dropdown.querySelector('.dropdown-menu').classList.add('hidden');
|
|
503
|
+
document.getElementById('agent-filters-form').requestSubmit();
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
function toggleAllOptions(checkbox, filterName) {
|
|
507
|
+
const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
|
|
508
|
+
const checkboxes = dropdown.querySelectorAll('.filter-checkbox');
|
|
509
|
+
|
|
510
|
+
if (checkbox.checked) {
|
|
511
|
+
checkboxes.forEach(cb => cb.checked = false);
|
|
512
|
+
updateMultiSelect(filterName);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
function updateMultiSelect(filterName) {
|
|
517
|
+
const dropdown = document.querySelector(`[data-filter="${filterName}"]`);
|
|
518
|
+
const checkboxes = dropdown.querySelectorAll('.filter-checkbox:checked');
|
|
519
|
+
const selectAllCheckbox = dropdown.querySelector('.select-all-checkbox');
|
|
520
|
+
const button = dropdown.querySelector('button');
|
|
521
|
+
const label = dropdown.querySelector('.dropdown-label');
|
|
522
|
+
|
|
523
|
+
const count = checkboxes.length;
|
|
524
|
+
|
|
525
|
+
selectAllCheckbox.checked = (count === 0);
|
|
526
|
+
|
|
527
|
+
const labelMap = {
|
|
528
|
+
'statuses': { all: 'All Statuses', plural: 'Statuses' },
|
|
529
|
+
'versions': { all: 'All Versions', plural: 'Versions' },
|
|
530
|
+
'models': { all: 'All Models', plural: 'Models' },
|
|
531
|
+
'temperatures': { all: 'All Temps', plural: 'Temps' }
|
|
532
|
+
};
|
|
533
|
+
|
|
534
|
+
if (count === 0) {
|
|
535
|
+
label.textContent = labelMap[filterName]?.all || 'All';
|
|
536
|
+
button.classList.remove('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
537
|
+
} else if (count === 1) {
|
|
538
|
+
const value = checkboxes[0].value;
|
|
539
|
+
if (filterName === 'statuses') {
|
|
540
|
+
label.textContent = value.charAt(0).toUpperCase() + value.slice(1);
|
|
541
|
+
} else if (filterName === 'versions') {
|
|
542
|
+
label.textContent = 'v' + value;
|
|
543
|
+
} else {
|
|
544
|
+
label.textContent = value;
|
|
545
|
+
}
|
|
546
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
547
|
+
} else {
|
|
548
|
+
label.textContent = `${count} ${labelMap[filterName]?.plural || 'Items'}`;
|
|
549
|
+
button.classList.add('ring-2', 'ring-blue-500', 'ring-offset-1');
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
document.getElementById('agent-filters-form').requestSubmit();
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
document.addEventListener('click', function(e) {
|
|
556
|
+
if (!e.target.closest('.filter-dropdown')) {
|
|
557
|
+
document.querySelectorAll('.filter-dropdown .dropdown-menu').forEach(menu => {
|
|
558
|
+
menu.classList.add('hidden');
|
|
559
|
+
});
|
|
560
|
+
}
|
|
561
|
+
});
|
|
562
|
+
</script>
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
<div id="execution-<%= execution.id %>" class="py-3 hover:bg-gray-50 -mx-2 px-2 rounded-lg transition-colors">
|
|
2
|
+
<%= link_to ruby_llm_agents.execution_path(execution), data: { turbo: false }, class: "block" do %>
|
|
3
|
+
<div class="flex items-start space-x-3">
|
|
4
|
+
<!-- Timeline indicator -->
|
|
5
|
+
<div class="flex flex-col items-center pt-1">
|
|
6
|
+
<%= render "rubyllm/agents/shared/status_dot", status: execution.status %>
|
|
7
|
+
</div>
|
|
8
|
+
|
|
9
|
+
<!-- Content -->
|
|
10
|
+
<div class="flex-1 min-w-0">
|
|
11
|
+
<div class="flex items-center justify-between">
|
|
12
|
+
<div class="flex items-center space-x-2">
|
|
13
|
+
<p class="font-medium text-gray-900 text-sm">
|
|
14
|
+
<%= execution.agent_type.gsub(/Agent$/, '') %>
|
|
15
|
+
</p>
|
|
16
|
+
<span class="text-xs text-gray-400">v<%= execution.agent_version %></span>
|
|
17
|
+
<% if execution.status_running? %>
|
|
18
|
+
<span class="text-xs text-blue-600 font-medium">Running...</span>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
<p class="text-xs text-gray-400">
|
|
22
|
+
<%= time_ago_in_words(execution.created_at) %> ago
|
|
23
|
+
</p>
|
|
24
|
+
</div>
|
|
25
|
+
|
|
26
|
+
<% if execution.status_running? %>
|
|
27
|
+
<p class="text-xs text-blue-500 mt-1">
|
|
28
|
+
In progress...
|
|
29
|
+
</p>
|
|
30
|
+
<% else %>
|
|
31
|
+
<p class="text-xs text-gray-500 mt-1">
|
|
32
|
+
<%= number_to_human_short(execution.total_tokens || 0) %> tokens
|
|
33
|
+
<span class="text-gray-300 mx-1">·</span>
|
|
34
|
+
<%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
|
|
35
|
+
<span class="text-gray-300 mx-1">·</span>
|
|
36
|
+
<%= number_to_human_short(execution.duration_ms || 0) %>ms
|
|
37
|
+
</p>
|
|
38
|
+
<% end %>
|
|
39
|
+
|
|
40
|
+
<% if execution.status_error? && execution.error_message.present? %>
|
|
41
|
+
<p class="text-xs text-red-600 mt-1.5 bg-red-50 rounded px-2 py-1">
|
|
42
|
+
<%= execution.error_class %>: <%= truncate(execution.error_message, length: 80) %>
|
|
43
|
+
</p>
|
|
44
|
+
<% end %>
|
|
45
|
+
</div>
|
|
46
|
+
</div>
|
|
47
|
+
<% end %>
|
|
48
|
+
</div>
|