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
@@ -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
  }
@@ -331,6 +187,35 @@
331
187
  .badge-orange {
332
188
  @apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
333
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
+ }
334
219
  </style>
335
220
  </head>
336
221
 
@@ -355,80 +240,17 @@
355
240
  <% end %>
356
241
 
357
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
+ %>
358
251
  <nav class="hidden md:flex items-center space-x-1">
359
- <%= 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 %>
360
- <svg
361
- class="w-4 h-4 mr-1.5"
362
- fill="none"
363
- stroke="currentColor"
364
- viewBox="0 0 24 24"
365
- >
366
- <path
367
- stroke-linecap="round"
368
- stroke-linejoin="round"
369
- stroke-width="2"
370
- 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"
371
- />
372
- </svg>
373
- Dashboard
374
- <% end %>
375
-
376
- <%= 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 %>
377
- <svg
378
- class="w-4 h-4 mr-1.5"
379
- fill="none"
380
- stroke="currentColor"
381
- viewBox="0 0 24 24"
382
- >
383
- <path
384
- stroke-linecap="round"
385
- stroke-linejoin="round"
386
- stroke-width="2"
387
- 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"
388
- />
389
- </svg>
390
- Agents
391
- <% end %>
392
-
393
- <%= 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 %>
394
- <svg
395
- class="w-4 h-4 mr-1.5"
396
- fill="none"
397
- stroke="currentColor"
398
- viewBox="0 0 24 24"
399
- >
400
- <path
401
- stroke-linecap="round"
402
- stroke-linejoin="round"
403
- stroke-width="2"
404
- 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"
405
- />
406
- </svg>
407
- Executions
408
- <% end %>
409
-
410
- <%= 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 %>
411
- <svg
412
- class="w-4 h-4 mr-1.5"
413
- fill="none"
414
- stroke="currentColor"
415
- viewBox="0 0 24 24"
416
- >
417
- <path
418
- stroke-linecap="round"
419
- stroke-linejoin="round"
420
- stroke-width="2"
421
- 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"
422
- />
423
-
424
- <path
425
- stroke-linecap="round"
426
- stroke-linejoin="round"
427
- stroke-width="2"
428
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
429
- />
430
- </svg>
431
- 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 %>
432
254
  <% end %>
433
255
  </nav>
434
256
  </div>
@@ -441,13 +263,16 @@
441
263
  >
442
264
  <span id="live-clock" class="tabular-nums"></span>
443
265
 
444
- <span
445
- id="live-indicator"
446
- 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"
447
271
  >
448
- <span class="w-1.5 h-1.5 bg-blue-500 rounded-full mr-1 animate-pulse"></span>
449
- <span class="hidden sm:inline">Auto</span>
450
- </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>
451
276
 
452
277
  <!-- Mobile menu button -->
453
278
  <button
@@ -501,79 +326,8 @@
501
326
  "
502
327
  >
503
328
  <nav class="max-w-7xl mx-auto px-4 py-3 space-y-1">
504
- <%= 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 %>
505
- <svg
506
- class="w-5 h-5 mr-3"
507
- fill="none"
508
- stroke="currentColor"
509
- viewBox="0 0 24 24"
510
- >
511
- <path
512
- stroke-linecap="round"
513
- stroke-linejoin="round"
514
- stroke-width="2"
515
- 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"
516
- />
517
- </svg>
518
- Dashboard
519
- <% end %>
520
-
521
- <%= 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 %>
522
- <svg
523
- class="w-5 h-5 mr-3"
524
- fill="none"
525
- stroke="currentColor"
526
- viewBox="0 0 24 24"
527
- >
528
- <path
529
- stroke-linecap="round"
530
- stroke-linejoin="round"
531
- stroke-width="2"
532
- 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"
533
- />
534
- </svg>
535
- Agents
536
- <% end %>
537
-
538
- <%= 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 %>
539
- <svg
540
- class="w-5 h-5 mr-3"
541
- fill="none"
542
- stroke="currentColor"
543
- viewBox="0 0 24 24"
544
- >
545
- <path
546
- stroke-linecap="round"
547
- stroke-linejoin="round"
548
- stroke-width="2"
549
- 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"
550
- />
551
- </svg>
552
- Executions
553
- <% end %>
554
-
555
- <%= 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 %>
556
- <svg
557
- class="w-5 h-5 mr-3"
558
- fill="none"
559
- stroke="currentColor"
560
- viewBox="0 0 24 24"
561
- >
562
- <path
563
- stroke-linecap="round"
564
- stroke-linejoin="round"
565
- stroke-width="2"
566
- 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"
567
- />
568
-
569
- <path
570
- stroke-linecap="round"
571
- stroke-linejoin="round"
572
- stroke-width="2"
573
- d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"
574
- />
575
- </svg>
576
- 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 %>
577
331
  <% end %>
578
332
  </nav>
579
333
  </div>
@@ -587,37 +341,25 @@
587
341
  <!-- Footer -->
588
342
  <footer
589
343
  class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto"
590
- data-controller="theme"
344
+ x-data="themeManager()"
591
345
  >
592
346
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
593
347
  <div class="flex flex-col sm:flex-row items-center sm:justify-between gap-3 sm:gap-0">
594
348
  <div class="flex items-center space-x-2">
595
- <label
596
- for="theme-select"
597
- class="text-sm text-gray-500 dark:text-gray-400"
598
- >
599
- Theme:
600
- </label>
601
-
349
+ <label for="theme-select" class="text-sm text-gray-500 dark:text-gray-400">Theme:</label>
602
350
  <select
603
351
  id="theme-select"
604
- data-theme-target="select"
605
- data-action="change->theme#change"
606
- class="
607
- text-sm border border-gray-300 dark:border-gray-600
608
- dark:bg-gray-700 dark:text-gray-200 rounded-md shadow-sm
609
- focus:ring-blue-500 focus:border-blue-500 py-1 px-2
610
- "
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"
611
355
  >
612
356
  <option value="light">Light</option>
613
357
  <option value="dark">Dark</option>
614
358
  <option value="auto">Auto</option>
615
359
  </select>
616
360
  </div>
617
-
618
361
  <p class="text-sm text-gray-500 dark:text-gray-400 text-center">
619
- Powered by
620
- <a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>
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>
621
363
  </p>
622
364
  </div>
623
365
  </div>