ruby_llm-agents 0.2.4 → 0.3.1

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 (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +413 -0
  3. data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
  4. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
  5. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
  6. data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
  9. data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
  10. data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
  12. data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
  13. data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
  14. data/app/models/ruby_llm/agents/execution.rb +259 -16
  15. data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
  16. data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
  17. data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
  18. data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
  19. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
  20. data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
  21. data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
  22. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
  23. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
  24. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
  25. data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
  26. data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
  27. data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
  28. data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
  29. data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
  30. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
  31. data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
  32. data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
  33. data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
  34. data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
  35. data/config/routes.rb +7 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
  37. data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
  39. data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
  41. data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
  42. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
  43. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
  44. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +207 -0
  48. data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
  49. data/lib/ruby_llm/agents/base.rb +597 -112
  50. data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
  51. data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
  52. data/lib/ruby_llm/agents/configuration.rb +279 -1
  53. data/lib/ruby_llm/agents/engine.rb +58 -6
  54. data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
  55. data/lib/ruby_llm/agents/inflections.rb +13 -2
  56. data/lib/ruby_llm/agents/instrumentation.rb +538 -87
  57. data/lib/ruby_llm/agents/redactor.rb +130 -0
  58. data/lib/ruby_llm/agents/reliability.rb +185 -0
  59. data/lib/ruby_llm/agents/version.rb +3 -1
  60. data/lib/ruby_llm/agents.rb +52 -0
  61. metadata +41 -2
  62. data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
@@ -4,32 +4,50 @@ module RubyLLM
4
4
  module Agents
5
5
  # Service for discovering and listing available agents
6
6
  #
7
- # Combines two sources:
7
+ # Combines two sources to ensure complete agent discovery:
8
8
  # 1. File system - Classes inheriting from ApplicationAgent in app/agents/
9
9
  # 2. Execution history - Agent types that have execution records
10
10
  #
11
- # This ensures all agents are visible, including:
12
- # - Agents that have never been executed
13
- # - Deleted agents that still have execution history
11
+ # This ensures visibility of both current agents and deleted agents
12
+ # that still have execution history.
14
13
  #
14
+ # @example Getting all agent names
15
+ # AgentRegistry.all #=> ["SearchAgent", "SummaryAgent"]
16
+ #
17
+ # @example Getting detailed info
18
+ # AgentRegistry.all_with_details.each do |agent|
19
+ # puts "#{agent[:name]}: #{agent[:execution_count]} executions"
20
+ # end
21
+ #
22
+ # @api public
15
23
  class AgentRegistry
16
24
  class << self
17
- # Returns all unique agent type names (sorted)
25
+ # Returns all unique agent type names
26
+ #
27
+ # @return [Array<String>] Sorted list of agent class names
18
28
  def all
19
29
  (file_system_agents + execution_agents).uniq.sort
20
30
  end
21
31
 
22
- # Returns agent class if it exists, nil if only in execution history
32
+ # Finds an agent class by type name
33
+ #
34
+ # @param agent_type [String] The agent class name
35
+ # @return [Class, nil] The agent class, or nil if not found
23
36
  def find(agent_type)
24
37
  agent_type.safe_constantize
25
38
  end
26
39
 
27
- # Check if an agent class is currently defined
40
+ # Checks if an agent class is currently defined
41
+ #
42
+ # @param agent_type [String] The agent class name
43
+ # @return [Boolean] true if the class exists
28
44
  def exists?(agent_type)
29
45
  find(agent_type).present?
30
46
  end
31
47
 
32
- # Get detailed info about all agents
48
+ # Returns detailed info about all agents
49
+ #
50
+ # @return [Array<Hash>] Agent info hashes with configuration and stats
33
51
  def all_with_details
34
52
  all.map do |agent_type|
35
53
  build_agent_info(agent_type)
@@ -38,7 +56,9 @@ module RubyLLM
38
56
 
39
57
  private
40
58
 
41
- # Find agent classes defined in the file system
59
+ # Finds agent classes from the file system
60
+ #
61
+ # @return [Array<String>] Agent class names
42
62
  def file_system_agents
43
63
  # Ensure all agent classes are loaded
44
64
  eager_load_agents!
@@ -51,7 +71,9 @@ module RubyLLM
51
71
  []
52
72
  end
53
73
 
54
- # Find agent types from execution history
74
+ # Finds agent types from execution history
75
+ #
76
+ # @return [Array<String>] Agent class names with execution records
55
77
  def execution_agents
56
78
  Execution.distinct.pluck(:agent_type).compact
57
79
  rescue StandardError => e
@@ -59,17 +81,24 @@ module RubyLLM
59
81
  []
60
82
  end
61
83
 
62
- # Eager load all agent files to ensure descendants are registered
84
+ # Eager loads all agent files to register descendants
85
+ #
86
+ # @return [void]
63
87
  def eager_load_agents!
64
88
  agents_path = Rails.root.join("app", "agents")
65
89
  return unless agents_path.exist?
66
90
 
67
91
  Dir.glob(agents_path.join("**", "*.rb")).each do |file|
68
92
  require_dependency file
93
+ rescue LoadError, StandardError => e
94
+ Rails.logger.error("[RubyLLM::Agents] Failed to load agent file #{file}: #{e.message}")
69
95
  end
70
96
  end
71
97
 
72
- # Build detailed info hash for an agent
98
+ # Builds detailed info hash for an agent
99
+ #
100
+ # @param agent_type [String] The agent class name
101
+ # @return [Hash] Agent info including config and stats
73
102
  def build_agent_info(agent_type)
74
103
  agent_class = find(agent_type)
75
104
  stats = fetch_stats(agent_type)
@@ -95,12 +124,20 @@ module RubyLLM
95
124
  }
96
125
  end
97
126
 
127
+ # Fetches statistics for an agent
128
+ #
129
+ # @param agent_type [String] The agent class name
130
+ # @return [Hash] Statistics hash
98
131
  def fetch_stats(agent_type)
99
132
  Execution.stats_for(agent_type, period: :all_time)
100
133
  rescue StandardError
101
134
  { count: 0, total_cost: 0, total_tokens: 0, avg_duration_ms: 0, success_rate: 0, error_rate: 0 }
102
135
  end
103
136
 
137
+ # Gets the timestamp of the last execution for an agent
138
+ #
139
+ # @param agent_type [String] The agent class name
140
+ # @return [Time, nil] Last execution time or nil
104
141
  def last_execution_time(agent_type)
105
142
  Execution.by_agent(agent_type).order(created_at: :desc).first&.created_at
106
143
  rescue StandardError
@@ -21,29 +21,169 @@
21
21
 
22
22
  <!-- Tailwind CSS via CDN -->
23
23
  <script src="https://cdn.tailwindcss.com"></script>
24
+
24
25
  <script>
25
26
  tailwind.config = {
26
27
  darkMode: 'class'
27
28
  }
28
29
  </script>
29
30
 
30
- <!-- Chartkick for charts -->
31
- <script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0"></script>
32
-
33
- <script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3.0.0"></script>
34
-
31
+ <!-- Highcharts for charts -->
32
+ <script src="https://code.highcharts.com/highcharts.js"></script>
35
33
  <script src="https://cdn.jsdelivr.net/npm/chartkick@5.0.1"></script>
36
34
 
35
+ <!-- Configure Highcharts defaults -->
36
+ <script>
37
+ Highcharts.setOptions({
38
+ credits: { enabled: false },
39
+ chart: {
40
+ backgroundColor: 'transparent',
41
+ style: { fontFamily: 'inherit' }
42
+ },
43
+ title: { text: null },
44
+ xAxis: {
45
+ labels: { style: { color: '#9CA3AF' } },
46
+ lineColor: 'rgba(156, 163, 175, 0.2)',
47
+ tickColor: 'rgba(156, 163, 175, 0.2)'
48
+ },
49
+ yAxis: {
50
+ labels: { style: { color: '#9CA3AF' } },
51
+ gridLineColor: 'rgba(156, 163, 175, 0.2)'
52
+ },
53
+ legend: {
54
+ itemStyle: { color: '#9CA3AF' },
55
+ itemHoverStyle: { color: '#D1D5DB' }
56
+ },
57
+ tooltip: {
58
+ backgroundColor: 'rgba(17, 24, 39, 0.9)',
59
+ borderColor: 'rgba(75, 85, 99, 0.5)',
60
+ style: { color: '#F3F4F6' }
61
+ }
62
+ });
63
+ </script>
64
+
37
65
  <!-- Stimulus -->
38
66
  <script
39
67
  src="https://unpkg.com/@hotwired/stimulus@3.2.2/dist/stimulus.umd.js"
40
68
  ></script>
41
69
 
42
- <!-- Turbo -->
43
- <%= javascript_include_tag "turbo", type: "module" rescue nil %>
70
+ <!-- Alpine.js -->
71
+ <script
72
+ defer
73
+ src="https://cdn.jsdelivr.net/npm/alpinejs@3.14.3/dist/cdn.min.js"
74
+ ></script>
75
+
76
+ <style>[x-cloak] { display: none !important; }</style>
44
77
 
45
- <!-- ActionCable (if available) -->
46
- <%= action_cable_meta_tag rescue nil %>
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>
47
187
 
48
188
  <!-- Live Clock Script -->
49
189
  <script type="module">
@@ -65,10 +205,19 @@
65
205
  if (window.rubyLLMAgentsClock) clearInterval(window.rubyLLMAgentsClock);
66
206
  updateClock();
67
207
  window.rubyLLMAgentsClock = setInterval(updateClock, 1000);
208
+
209
+ // Handle data-href clickable rows (semantic alternative to onclick)
210
+ document.querySelectorAll('[data-href]').forEach(function(element) {
211
+ element.addEventListener('click', function(e) {
212
+ // Don't navigate if clicking on a link or button inside the row
213
+ if (e.target.closest('a, button')) return;
214
+ window.location = element.dataset.href;
215
+ });
216
+ });
68
217
  });
69
218
  </script>
70
219
 
71
- <!-- Stimulus Connection Controller -->
220
+ <!-- Stimulus Controllers -->
72
221
  <script>
73
222
  (function() {
74
223
  // Initialize Stimulus immediately (works with Turbo)
@@ -82,63 +231,6 @@
82
231
  if (!Stimulus.application) {
83
232
  Stimulus.application = Stimulus.Application.start();
84
233
 
85
- // Polling Controller - simple auto-refresh
86
- Stimulus.application.register("connection", class extends Stimulus.Controller {
87
- static targets = ["indicator", "dashboardFrame"]
88
-
89
- static values = {
90
- pollInterval: { type: Number, default: 4000 }
91
- }
92
-
93
- connect() {
94
- this.pollTimer = null;
95
- this.startPolling();
96
- }
97
-
98
- disconnect() {
99
- this.stopPolling();
100
- }
101
-
102
- startPolling() {
103
- if (this.pollTimer) return;
104
-
105
- this.updateIndicator('polling');
106
-
107
- this.pollTimer = setInterval(() => {
108
- this.poll();
109
- }, this.pollIntervalValue);
110
- }
111
-
112
- stopPolling() {
113
- if (this.pollTimer) {
114
- clearInterval(this.pollTimer);
115
- this.pollTimer = null;
116
- }
117
- }
118
-
119
- poll() {
120
- if (this.hasDashboardFrameTarget) {
121
- const frame = this.dashboardFrameTarget;
122
- if (frame.src) {
123
- frame.reload();
124
- } else {
125
- frame.src = window.location.href;
126
- }
127
- }
128
- }
129
-
130
- updateIndicator(status) {
131
- if (!this.hasIndicatorTarget) return;
132
-
133
- const indicator = this.indicatorTarget;
134
- indicator.innerHTML = `
135
- <span class="w-1.5 h-1.5 bg-blue-500 rounded-full mr-1"></span>
136
- <span class="hidden sm:inline">Polling</span>
137
- `;
138
- indicator.className = 'flex items-center text-blue-600';
139
- }
140
- });
141
-
142
234
  // Theme Controller - handles dark mode switching
143
235
  Stimulus.application.register("theme", class extends Stimulus.Controller {
144
236
  static targets = ["select"]
@@ -233,21 +325,28 @@
233
325
  </style>
234
326
  </head>
235
327
 
236
- <body
237
- class="bg-gray-50 dark:bg-gray-900 min-h-screen flex flex-col"
238
- data-controller="connection"
239
- >
328
+ <body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex flex-col">
240
329
  <!-- Header -->
241
- <header class="bg-white dark:bg-gray-800 border-b border-gray-200 dark:border-gray-700 h-16 flex items-center">
242
- <div class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8">
243
- <div class="flex justify-between items-center">
330
+ <header
331
+ class="
332
+ bg-white dark:bg-gray-800 border-b border-gray-200
333
+ dark:border-gray-700
334
+ "
335
+ x-data="{ mobileMenuOpen: false }"
336
+ >
337
+ <div class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center">
338
+ <div class="flex justify-between items-center w-full">
244
339
  <div class="flex items-center space-x-8">
245
340
  <%= link_to ruby_llm_agents.root_path, class: "flex items-center space-x-2" do %>
246
341
  <span class="text-lg">🤖</span>
247
- <span class="font-semibold text-gray-900 dark:text-gray-100">RubyLLM Agents</span>
342
+
343
+ <span class="font-semibold text-gray-900 dark:text-gray-100">
344
+ RubyLLM Agents
345
+ </span>
248
346
  <% end %>
249
347
 
250
- <nav class="flex items-center space-x-1">
348
+ <!-- Desktop Navigation -->
349
+ <nav class="hidden md:flex items-center space-x-1">
251
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 %>
252
351
  <svg
253
352
  class="w-4 h-4 mr-1.5"
@@ -298,24 +397,177 @@
298
397
  </svg>
299
398
  Executions
300
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
423
+ <% end %>
301
424
  </nav>
302
425
  </div>
303
426
 
304
- <div class="flex items-center space-x-2 text-xs text-gray-500 dark:text-gray-400">
427
+ <div
428
+ class="
429
+ flex items-center space-x-2 text-xs text-gray-500
430
+ dark:text-gray-400
431
+ "
432
+ >
305
433
  <span id="live-clock" class="tabular-nums"></span>
306
- <span class="text-gray-300 dark:text-gray-600">•</span>
307
434
 
308
435
  <span
309
436
  id="live-indicator"
310
- data-connection-target="indicator"
311
- class="flex items-center text-gray-400 dark:text-gray-500"
437
+ class="hidden sm:flex items-center text-blue-600 dark:text-blue-400"
312
438
  >
313
- <span class="w-1.5 h-1.5 bg-gray-300 dark:bg-gray-600 rounded-full mr-1"></span>
314
- <span class="hidden sm:inline">Connecting</span>
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>
315
441
  </span>
442
+
443
+ <!-- Mobile menu button -->
444
+ <button
445
+ type="button"
446
+ @click="mobileMenuOpen = !mobileMenuOpen"
447
+ :aria-expanded="mobileMenuOpen"
448
+ aria-controls="mobile-menu"
449
+ class="
450
+ md:hidden inline-flex items-center justify-center p-2
451
+ rounded-md text-gray-500 dark:text-gray-400
452
+ hover:text-gray-900 dark:hover:text-gray-100
453
+ hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none
454
+ focus:ring-2 focus:ring-inset focus:ring-blue-500
455
+ "
456
+ >
457
+ <span class="sr-only">Open main menu</span>
458
+
459
+ <svg
460
+ class="w-5 h-5"
461
+ fill="none"
462
+ stroke="currentColor"
463
+ viewBox="0 0 24 24"
464
+ >
465
+ <path
466
+ stroke-linecap="round"
467
+ stroke-linejoin="round"
468
+ stroke-width="2"
469
+ d="M4 6h16M4 12h16M4 18h16"
470
+ />
471
+ </svg>
472
+ </button>
316
473
  </div>
317
474
  </div>
318
475
  </div>
476
+
477
+ <!-- Mobile Navigation Menu -->
478
+ <div
479
+ id="mobile-menu"
480
+ x-show="mobileMenuOpen"
481
+ x-cloak
482
+ x-transition:enter="transition ease-out duration-200"
483
+ x-transition:enter-start="opacity-0 -translate-y-1"
484
+ x-transition:enter-end="opacity-100 translate-y-0"
485
+ x-transition:leave="transition ease-in duration-150"
486
+ x-transition:leave-start="opacity-100 translate-y-0"
487
+ x-transition:leave-end="opacity-0 -translate-y-1"
488
+ @click.outside="mobileMenuOpen = false"
489
+ class="
490
+ md:hidden border-t border-gray-200 dark:border-gray-700 bg-white
491
+ dark:bg-gray-800
492
+ "
493
+ >
494
+ <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
568
+ <% end %>
569
+ </nav>
570
+ </div>
319
571
  </header>
320
572
 
321
573
  <!-- Main content -->
@@ -324,22 +576,36 @@
324
576
  </main>
325
577
 
326
578
  <!-- Footer -->
327
- <footer class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto" data-controller="theme">
579
+ <footer
580
+ class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto"
581
+ data-controller="theme"
582
+ >
328
583
  <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
329
584
  <div class="flex items-center justify-between">
330
585
  <div class="flex items-center space-x-2">
331
- <label for="theme-select" class="text-sm text-gray-500 dark:text-gray-400">Theme:</label>
586
+ <label
587
+ for="theme-select"
588
+ class="text-sm text-gray-500 dark:text-gray-400"
589
+ >
590
+ Theme:
591
+ </label>
592
+
332
593
  <select
333
594
  id="theme-select"
334
595
  data-theme-target="select"
335
596
  data-action="change->theme#change"
336
- 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"
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
+ "
337
602
  >
338
603
  <option value="light">Light</option>
339
604
  <option value="dark">Dark</option>
340
605
  <option value="auto">Auto</option>
341
606
  </select>
342
607
  </div>
608
+
343
609
  <p class="text-sm text-gray-500 dark:text-gray-400">
344
610
  Powered by
345
611
  <a href="https://github.com/adham90/ruby_llm-agents" class="text-blue-600 dark:text-blue-400 hover:underline">ruby_llm-agents</a>