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
@@ -30,7 +30,6 @@
30
30
 
31
31
  <!-- Highcharts for charts -->
32
32
  <script src="https://code.highcharts.com/highcharts.js"></script>
33
- <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1"></script>
34
33
 
35
34
  <!-- Configure Highcharts defaults -->
36
35
  <script>
@@ -62,11 +61,6 @@
62
61
  });
63
62
  </script>
64
63
 
65
- <!-- Stimulus -->
66
- <script
67
- src="https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js"
68
- ></script>
69
-
70
64
  <!-- Alpine.js -->
71
65
  <script
72
66
  defer
@@ -75,117 +69,7 @@
75
69
 
76
70
  <style>[x-cloak] { display: none !important; }</style>
77
71
 
78
- <!-- Auto-refresh for dashboard updates -->
79
- <script>
80
- (function() {
81
- // Simple polling for real-time updates (5 second interval)
82
- const POLL_INTERVAL = 5000
83
- let pollTimer = null
84
-
85
- function startPolling() {
86
- if (pollTimer) return
87
-
88
- // Update indicator to show polling is active
89
- const indicator = document.getElementById("live-indicator")
90
- if (indicator) {
91
- indicator.className = "hidden sm:flex items-center text-blue-600 dark:text-blue-400"
92
- indicator.innerHTML = '<span class="w-1.5 h-1.5 bg-blue-500 rounded-full mr-1 animate-pulse"></span><span class="hidden sm:inline">Auto</span>'
93
- }
94
-
95
- pollTimer = setInterval(() => {
96
- // Dashboard page - detected by presence of activity feed
97
- if (document.getElementById("activity-feed")) {
98
- fetch(window.location.href, {
99
- headers: { "Accept": "text/html" }
100
- })
101
- .then(response => response.text())
102
- .then(html => {
103
- const parser = new DOMParser()
104
- const doc = parser.parseFromString(html, "text/html")
105
-
106
- // Update activity feed
107
- const newFeed = doc.getElementById("activity-feed")
108
- const currentFeed = document.getElementById("activity-feed")
109
- if (newFeed && currentFeed) {
110
- currentFeed.innerHTML = newFeed.innerHTML
111
- }
112
-
113
- // Update now strip
114
- const newStrip = doc.getElementById("now-strip-values")
115
- const currentStrip = document.getElementById("now-strip-values")
116
- if (newStrip && currentStrip) {
117
- currentStrip.innerHTML = newStrip.innerHTML
118
- }
119
-
120
- // Note: Chart has its own 1-second live update built-in
121
-
122
- // Update action center
123
- const newActionCenter = doc.getElementById("action-center")
124
- const currentActionCenter = document.getElementById("action-center")
125
- if (newActionCenter && currentActionCenter) {
126
- currentActionCenter.outerHTML = newActionCenter.outerHTML
127
- } else if (newActionCenter && !currentActionCenter) {
128
- // Insert action center if it appeared
129
- const main = document.querySelector("main")
130
- if (main) main.insertAdjacentHTML("afterbegin", newActionCenter.outerHTML)
131
- } else if (!newActionCenter && currentActionCenter) {
132
- // Remove action center if it disappeared
133
- currentActionCenter.remove()
134
- }
135
- })
136
- .catch(err => console.log("[RubyLLM::Agents] Poll error:", err))
137
- }
138
-
139
- // Execution show page - detected by presence of execution-detail element
140
- const executionDetail = document.getElementById("execution-detail")
141
- if (executionDetail) {
142
- const currentStatus = executionDetail.dataset.status
143
-
144
- // Only poll if execution is still running
145
- if (currentStatus === "running") {
146
- fetch(window.location.href, {
147
- headers: { "Accept": "text/html" }
148
- })
149
- .then(response => response.text())
150
- .then(html => {
151
- const parser = new DOMParser()
152
- const doc = parser.parseFromString(html, "text/html")
153
-
154
- // Update the main content area when status changes
155
- const newContent = doc.getElementById("execution-detail")
156
- if (newContent && newContent.dataset.status !== "running") {
157
- // Execution completed - do a full content update
158
- executionDetail.outerHTML = newContent.outerHTML
159
- }
160
- })
161
- .catch(err => console.log("[RubyLLM::Agents] Poll error:", err))
162
- }
163
- }
164
- }, POLL_INTERVAL)
165
- }
166
-
167
- function stopPolling() {
168
- if (pollTimer) {
169
- clearInterval(pollTimer)
170
- pollTimer = null
171
- }
172
- }
173
-
174
- // Start polling on page load
175
- document.addEventListener("DOMContentLoaded", startPolling)
176
-
177
- // Handle visibility changes - pause when tab is hidden
178
- document.addEventListener("visibilitychange", () => {
179
- if (document.hidden) {
180
- stopPolling()
181
- } else {
182
- startPolling()
183
- }
184
- })
185
- })()
186
- </script>
187
-
188
- <!-- Live Clock Script -->
72
+ <!-- Clock and clickable rows -->
189
73
  <script type="module">
190
74
  function updateClock() {
191
75
  const clock = document.getElementById('live-clock');
@@ -194,17 +78,14 @@
194
78
  clock.textContent = now.toLocaleTimeString('en-US', {
195
79
  hour: '2-digit',
196
80
  minute: '2-digit',
197
- second: '2-digit',
198
81
  hour12: false
199
82
  });
200
83
  }
201
84
  }
202
85
 
203
- // Start clock on page load
86
+ // Initialize on page load
204
87
  document.addEventListener('turbo:load', function() {
205
- if (window.rubyLLMAgentsClock) clearInterval(window.rubyLLMAgentsClock);
206
88
  updateClock();
207
- window.rubyLLMAgentsClock = setInterval(updateClock, 1000);
208
89
 
209
90
  // Handle data-href clickable rows (semantic alternative to onclick)
210
91
  document.querySelectorAll('[data-href]').forEach(function(element) {
@@ -215,72 +96,47 @@
215
96
  });
216
97
  });
217
98
  });
99
+
100
+ // Also run on DOMContentLoaded for non-Turbo loads
101
+ document.addEventListener('DOMContentLoaded', updateClock);
218
102
  </script>
219
103
 
220
- <!-- Stimulus Controllers -->
104
+ <!-- Theme Switcher (Alpine.js) -->
221
105
  <script>
222
- (function() {
223
- // Initialize Stimulus immediately (works with Turbo)
224
- if (!window.Stimulus) {
225
- window.Stimulus = {};
226
- }
227
-
228
- const Stimulus = window.Stimulus;
229
-
230
- // Only initialize once
231
- if (!Stimulus.application) {
232
- Stimulus.application = Stimulus.Application.start();
233
-
234
- // Theme Controller - handles dark mode switching
235
- Stimulus.application.register("theme", class extends Stimulus.Controller {
236
- static targets = ["select"]
237
-
238
- connect() {
239
- this.mediaQuery = window.matchMedia('(prefers-color-scheme: dark)')
240
- this.applyTheme(this.getPreference())
241
- this.boundHandleSystemChange = this.handleSystemChange.bind(this)
242
- this.mediaQuery.addEventListener('change', this.boundHandleSystemChange)
243
-
244
- // Set the select value to match stored preference
245
- if (this.hasSelectTarget) {
246
- this.selectTarget.value = this.getPreference()
247
- }
248
- }
249
-
250
- disconnect() {
251
- this.mediaQuery.removeEventListener('change', this.boundHandleSystemChange)
252
- }
253
-
254
- change(event) {
255
- const preference = event.target.value
256
- localStorage.setItem('ruby_llm_agents_theme', preference)
257
- this.applyTheme(preference)
258
- }
259
-
260
- getPreference() {
261
- return localStorage.getItem('ruby_llm_agents_theme') || 'auto'
262
- }
263
-
264
- applyTheme(preference) {
265
- const isDark = preference === 'dark' ||
266
- (preference === 'auto' && this.mediaQuery.matches)
267
- document.documentElement.classList.toggle('dark', isDark)
268
- }
269
-
270
- handleSystemChange() {
271
- if (this.getPreference() === 'auto') {
272
- this.applyTheme('auto')
273
- }
274
- }
275
- });
276
-
277
- console.log('[RubyLLM::Agents] Stimulus initialized');
278
- }
279
- })();
106
+ function themeManager() {
107
+ return {
108
+ preference: localStorage.getItem('ruby_llm_agents_theme') || 'auto',
109
+ init() {
110
+ this.applyTheme();
111
+ window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
112
+ if (this.preference === 'auto') this.applyTheme();
113
+ });
114
+ },
115
+ setTheme(value) {
116
+ this.preference = value;
117
+ localStorage.setItem('ruby_llm_agents_theme', value);
118
+ this.applyTheme();
119
+ },
120
+ applyTheme() {
121
+ const isDark = this.preference === 'dark' ||
122
+ (this.preference === 'auto' && window.matchMedia('(prefers-color-scheme: dark)').matches);
123
+ document.documentElement.classList.toggle('dark', isDark);
124
+ }
125
+ };
126
+ }
280
127
  </script>
281
128
 
282
129
  <style>
283
130
  /* Custom styles for the dashboard */
131
+
132
+ /* Standard card components */
133
+ .card {
134
+ @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6;
135
+ }
136
+ .card-compact {
137
+ @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 mb-4;
138
+ }
139
+
284
140
  .stat-card {
285
141
  @apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 transition-shadow;
286
142
  }
@@ -322,6 +178,44 @@
322
178
  .badge-timeout {
323
179
  @apply bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200;
324
180
  }
181
+ .badge-cyan {
182
+ @apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
183
+ }
184
+ .badge-purple {
185
+ @apply bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300;
186
+ }
187
+ .badge-orange {
188
+ @apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
189
+ }
190
+
191
+ /* Turbo loading states */
192
+ turbo-frame[busy] {
193
+ position: relative;
194
+ opacity: 0.6;
195
+ pointer-events: none;
196
+ }
197
+ turbo-frame[busy]::after {
198
+ content: '';
199
+ position: absolute;
200
+ top: 50%;
201
+ left: 50%;
202
+ width: 24px;
203
+ height: 24px;
204
+ margin: -12px 0 0 -12px;
205
+ border: 2px solid #e5e7eb;
206
+ border-top-color: #3b82f6;
207
+ border-radius: 50%;
208
+ animation: spin 0.8s linear infinite;
209
+ }
210
+ @keyframes spin {
211
+ to { transform: rotate(360deg); }
212
+ }
213
+
214
+ /* Form submitting state */
215
+ form[data-turbo-submitting] {
216
+ opacity: 0.6;
217
+ pointer-events: none;
218
+ }
325
219
  </style>
326
220
  </head>
327
221
 
@@ -346,80 +240,17 @@
346
240
  <% end %>
347
241
 
348
242
  <!-- Desktop Navigation -->
243
+ <%
244
+ nav_items = [
245
+ { path: ruby_llm_agents.root_path, label: "Dashboard", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6" />' },
246
+ { path: ruby_llm_agents.agents_path, label: "Agents", icon: '<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" />' },
247
+ { path: ruby_llm_agents.executions_path, label: "Executions", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />' },
248
+ { path: ruby_llm_agents.settings_path, label: "Settings", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" /><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />' }
249
+ ]
250
+ %>
349
251
  <nav class="hidden md:flex items-center space-x-1">
350
- <%= link_to ruby_llm_agents.root_path, class: "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md #{current_page?(ruby_llm_agents.root_path) ? 'bg-gray-200 dark:bg-gray-700 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
351
- <svg
352
- class="w-4 h-4 mr-1.5"
353
- fill="none"
354
- stroke="currentColor"
355
- viewBox="0 0 24 24"
356
- >
357
- <path
358
- stroke-linecap="round"
359
- stroke-linejoin="round"
360
- stroke-width="2"
361
- d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
362
- />
363
- </svg>
364
- Dashboard
365
- <% end %>
366
-
367
- <%= link_to ruby_llm_agents.agents_path, class: "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md #{request.path.start_with?(ruby_llm_agents.agents_path) ? 'bg-gray-200 dark:bg-gray-700 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
368
- <svg
369
- class="w-4 h-4 mr-1.5"
370
- fill="none"
371
- stroke="currentColor"
372
- viewBox="0 0 24 24"
373
- >
374
- <path
375
- stroke-linecap="round"
376
- stroke-linejoin="round"
377
- stroke-width="2"
378
- 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"
379
- />
380
- </svg>
381
- Agents
382
- <% end %>
383
-
384
- <%= link_to ruby_llm_agents.executions_path, class: "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md #{current_page?(ruby_llm_agents.executions_path) ? 'bg-gray-200 dark:bg-gray-700 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
385
- <svg
386
- class="w-4 h-4 mr-1.5"
387
- fill="none"
388
- stroke="currentColor"
389
- viewBox="0 0 24 24"
390
- >
391
- <path
392
- stroke-linecap="round"
393
- stroke-linejoin="round"
394
- stroke-width="2"
395
- d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
396
- />
397
- </svg>
398
- Executions
399
- <% end %>
400
-
401
- <%= link_to ruby_llm_agents.settings_path, class: "inline-flex items-center px-3 py-1.5 text-sm font-medium rounded-md #{current_page?(ruby_llm_agents.settings_path) ? 'bg-gray-200 dark:bg-gray-700 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
402
- <svg
403
- class="w-4 h-4 mr-1.5"
404
- fill="none"
405
- stroke="currentColor"
406
- viewBox="0 0 24 24"
407
- >
408
- <path
409
- stroke-linecap="round"
410
- stroke-linejoin="round"
411
- stroke-width="2"
412
- d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
413
- />
414
-
415
- <path
416
- stroke-linecap="round"
417
- stroke-linejoin="round"
418
- stroke-width="2"
419
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
420
- />
421
- </svg>
422
- Settings
252
+ <% nav_items.each do |item| %>
253
+ <%= render "rubyllm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: false %>
423
254
  <% end %>
424
255
  </nav>
425
256
  </div>
@@ -432,13 +263,16 @@
432
263
  >
433
264
  <span id="live-clock" class="tabular-nums"></span>
434
265
 
435
- <span
436
- id="live-indicator"
437
- class="hidden sm:flex items-center text-blue-600 dark:text-blue-400"
266
+ <button
267
+ id="refresh-button"
268
+ onclick="window.location.reload()"
269
+ title="Refresh page"
270
+ class="p-1.5 rounded-md text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
438
271
  >
439
- <span class="w-1.5 h-1.5 bg-blue-500 rounded-full mr-1 animate-pulse"></span>
440
- <span class="hidden sm:inline">Auto</span>
441
- </span>
272
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
273
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
274
+ </svg>
275
+ </button>
442
276
 
443
277
  <!-- Mobile menu button -->
444
278
  <button
@@ -492,79 +326,8 @@
492
326
  "
493
327
  >
494
328
  <nav class="max-w-7xl mx-auto px-4 py-3 space-y-1">
495
- <%= link_to ruby_llm_agents.root_path, "x-on:click": "mobileMenuOpen = false", class: "flex items-center px-3 py-2 text-base font-medium rounded-md #{current_page?(ruby_llm_agents.root_path) ? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
496
- <svg
497
- class="w-5 h-5 mr-3"
498
- fill="none"
499
- stroke="currentColor"
500
- viewBox="0 0 24 24"
501
- >
502
- <path
503
- stroke-linecap="round"
504
- stroke-linejoin="round"
505
- stroke-width="2"
506
- d="M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"
507
- />
508
- </svg>
509
- Dashboard
510
- <% end %>
511
-
512
- <%= link_to ruby_llm_agents.agents_path, "x-on:click": "mobileMenuOpen = false", class: "flex items-center px-3 py-2 text-base font-medium rounded-md #{request.path.start_with?(ruby_llm_agents.agents_path) ? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
513
- <svg
514
- class="w-5 h-5 mr-3"
515
- fill="none"
516
- stroke="currentColor"
517
- viewBox="0 0 24 24"
518
- >
519
- <path
520
- stroke-linecap="round"
521
- stroke-linejoin="round"
522
- stroke-width="2"
523
- 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"
524
- />
525
- </svg>
526
- Agents
527
- <% end %>
528
-
529
- <%= link_to ruby_llm_agents.executions_path, "x-on:click": "mobileMenuOpen = false", class: "flex items-center px-3 py-2 text-base font-medium rounded-md #{current_page?(ruby_llm_agents.executions_path) ? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
530
- <svg
531
- class="w-5 h-5 mr-3"
532
- fill="none"
533
- stroke="currentColor"
534
- viewBox="0 0 24 24"
535
- >
536
- <path
537
- stroke-linecap="round"
538
- stroke-linejoin="round"
539
- stroke-width="2"
540
- d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01"
541
- />
542
- </svg>
543
- Executions
544
- <% end %>
545
-
546
- <%= link_to ruby_llm_agents.settings_path, "x-on:click": "mobileMenuOpen = false", class: "flex items-center px-3 py-2 text-base font-medium rounded-md #{current_page?(ruby_llm_agents.settings_path) ? 'bg-gray-200 dark:bg-gray-700 text-gray-900 dark:text-gray-100' : 'text-gray-600 dark:text-gray-300 hover:text-gray-900 dark:hover:text-gray-100 hover:bg-gray-100 dark:hover:bg-gray-700'}" do %>
547
- <svg
548
- class="w-5 h-5 mr-3"
549
- fill="none"
550
- stroke="currentColor"
551
- viewBox="0 0 24 24"
552
- >
553
- <path
554
- stroke-linecap="round"
555
- stroke-linejoin="round"
556
- stroke-width="2"
557
- d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z"
558
- />
559
-
560
- <path
561
- stroke-linecap="round"
562
- stroke-linejoin="round"
563
- stroke-width="2"
564
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
565
- />
566
- </svg>
567
- Settings
329
+ <% nav_items.each do |item| %>
330
+ <%= render "rubyllm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
568
331
  <% end %>
569
332
  </nav>
570
333
  </div>
@@ -578,37 +341,25 @@
578
341
  <!-- Footer -->
579
342
  <footer
580
343
  class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto"
581
- data-controller="theme"
344
+ x-data="themeManager()"
582
345
  >
583
346
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
584
- <div class="flex items-center justify-between">
347
+ <div class="flex flex-col sm:flex-row items-center sm:justify-between gap-3 sm:gap-0">
585
348
  <div class="flex items-center space-x-2">
586
- <label
587
- for="theme-select"
588
- class="text-sm text-gray-500 dark:text-gray-400"
589
- >
590
- Theme:
591
- </label>
592
-
349
+ <label for="theme-select" class="text-sm text-gray-500 dark:text-gray-400">Theme:</label>
593
350
  <select
594
351
  id="theme-select"
595
- data-theme-target="select"
596
- data-action="change->theme#change"
597
- class="
598
- text-sm border border-gray-300 dark:border-gray-600
599
- dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm
600
- focus:ring-blue-500 focus:border-blue-500 py-1 px-2
601
- "
352
+ x-model="preference"
353
+ @change="setTheme($event.target.value)"
354
+ class="text-sm border border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm focus:ring-blue-500 focus:border-blue-500 py-1 px-2"
602
355
  >
603
356
  <option value="light">Light</option>
604
357
  <option value="dark">Dark</option>
605
358
  <option value="auto">Auto</option>
606
359
  </select>
607
360
  </div>
608
-
609
- <p class="text-sm text-gray-500 dark:text-gray-400">
610
- Powered by
611
- <a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>
361
+ <p class="text-sm text-gray-500 dark:text-gray-400 text-center">
362
+ Powered by <a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>
612
363
  </p>
613
364
  </div>
614
365
  </div>
@@ -0,0 +1,87 @@
1
+ <%= link_to ruby_llm_agents.agent_path(agent[:name]), class: "block bg-white dark:bg-gray-800 rounded-lg shadow hover:shadow-md transition-shadow" do %>
2
+ <div class="p-4 sm:p-5">
3
+ <!-- Row 1: Agent name + badge + model/timestamp -->
4
+ <div class="flex items-center justify-between gap-2">
5
+ <div class="flex items-center gap-2 min-w-0">
6
+ <h3 class="font-semibold text-gray-900 dark:text-gray-100 truncate">
7
+ <%= agent[:name].gsub(/Agent$/, '') %>
8
+ </h3>
9
+ <span class="hidden sm:inline text-sm text-gray-500 dark:text-gray-400">v<%= agent[:version] %></span>
10
+ <% if agent[:active] %>
11
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-green-100 dark:bg-green-900/50 text-green-800 dark:text-green-300">Active</span>
12
+ <% else %>
13
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-400">Deleted</span>
14
+ <% end %>
15
+ </div>
16
+ <!-- Desktop: model -->
17
+ <span class="hidden sm:block text-sm text-gray-500 dark:text-gray-400"><%= agent[:model] %></span>
18
+ <!-- Mobile: last executed -->
19
+ <span class="sm:hidden text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap">
20
+ <% if agent[:last_executed] %>
21
+ <%= time_ago_in_words(agent[:last_executed]) %> ago
22
+ <% else %>
23
+ Never
24
+ <% end %>
25
+ </span>
26
+ </div>
27
+
28
+ <!-- Row 2: Stats -->
29
+ <div class="mt-2 sm:mt-3 sm:border-t sm:border-gray-100 sm:dark:border-gray-700 sm:pt-3">
30
+ <!-- Mobile: compact inline -->
31
+ <% success_rate = agent[:success_rate] || 0 %>
32
+ <div class="sm:hidden text-xs text-gray-500 dark:text-gray-400">
33
+ <%= number_with_delimiter(agent[:execution_count]) %> runs
34
+ <span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
35
+ $<%= number_with_precision(agent[:total_cost] || 0, precision: 2) %>
36
+ <span class="mx-1 text-gray-300 dark:text-gray-600">·</span>
37
+ <span class="<%= success_rate >= 95 ? 'text-green-600 dark:text-green-400' : success_rate >= 80 ? 'text-yellow-600 dark:text-yellow-400' : 'text-red-600 dark:text-red-400' %>">
38
+ <%= success_rate %>%
39
+ </span>
40
+ </div>
41
+
42
+ <!-- Desktop: full stats with icons -->
43
+ <div class="hidden sm:flex items-center justify-between text-sm">
44
+ <div class="flex items-center space-x-6">
45
+ <!-- Executions -->
46
+ <div class="flex items-center text-gray-600 dark:text-gray-300">
47
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
48
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z" />
49
+ </svg>
50
+ <span><%= number_with_delimiter(agent[:execution_count]) %> executions</span>
51
+ </div>
52
+
53
+ <!-- Cost -->
54
+ <div class="flex items-center text-gray-600 dark:text-gray-300">
55
+ <svg class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
56
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="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" />
57
+ </svg>
58
+ <span>$<%= number_with_precision(agent[:total_cost] || 0, precision: 4) %></span>
59
+ </div>
60
+
61
+ <!-- Success Rate -->
62
+ <div class="flex items-center">
63
+ <% if success_rate >= 95 %>
64
+ <span class="w-2 h-2 rounded-full bg-green-500 mr-1.5"></span>
65
+ <span class="text-green-600 dark:text-green-400"><%= success_rate %>%</span>
66
+ <% elsif success_rate >= 80 %>
67
+ <span class="w-2 h-2 rounded-full bg-yellow-500 mr-1.5"></span>
68
+ <span class="text-yellow-600 dark:text-yellow-400"><%= success_rate %>%</span>
69
+ <% else %>
70
+ <span class="w-2 h-2 rounded-full bg-red-500 mr-1.5"></span>
71
+ <span class="text-red-600 dark:text-red-400"><%= success_rate %>%</span>
72
+ <% end %>
73
+ </div>
74
+ </div>
75
+
76
+ <!-- Last Executed -->
77
+ <div class="text-gray-400 dark:text-gray-500">
78
+ <% if agent[:last_executed] %>
79
+ <%= time_ago_in_words(agent[:last_executed]) %> ago
80
+ <% else %>
81
+ Never executed
82
+ <% end %>
83
+ </div>
84
+ </div>
85
+ </div>
86
+ </div>
87
+ <% end %>