ruby_llm-agents 1.3.4 → 2.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.
Files changed (191) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +112 -336
  3. data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
  4. data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
  6. data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
  8. data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
  9. data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
  10. data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
  11. data/app/models/ruby_llm/agents/execution.rb +52 -12
  12. data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
  13. data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
  14. data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
  15. data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
  16. data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
  17. data/app/models/ruby_llm/agents/tenant.rb +2 -3
  18. data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
  19. data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
  20. data/app/views/layouts/ruby_llm/agents/application.html.erb +89 -252
  21. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
  22. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
  23. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
  24. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
  25. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
  26. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
  27. data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
  28. data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
  29. data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
  30. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
  31. data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
  32. data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
  33. data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
  34. data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
  35. data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
  36. data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
  37. data/app/views/ruby_llm/agents/executions/show.html.erb +526 -1037
  38. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
  39. data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
  40. data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
  41. data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
  42. data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
  43. data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
  44. data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
  45. data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
  46. data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
  47. data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
  48. data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
  49. data/config/routes.rb +0 -13
  50. data/lib/generators/ruby_llm_agents/install_generator.rb +13 -17
  51. data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
  52. data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
  53. data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
  54. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
  55. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
  56. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
  57. data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
  58. data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
  59. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
  60. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +33 -12
  61. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
  62. data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
  63. data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
  64. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
  65. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
  66. data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
  67. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +77 -259
  68. data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
  69. data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
  70. data/lib/ruby_llm/agents/base_agent.rb +54 -23
  71. data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
  72. data/lib/ruby_llm/agents/core/base.rb +23 -55
  73. data/lib/ruby_llm/agents/core/configuration.rb +97 -117
  74. data/lib/ruby_llm/agents/core/errors.rb +0 -58
  75. data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
  76. data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
  77. data/lib/ruby_llm/agents/core/version.rb +1 -1
  78. data/lib/ruby_llm/agents/dsl/base.rb +157 -17
  79. data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
  80. data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
  81. data/lib/ruby_llm/agents/dsl.rb +1 -2
  82. data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
  83. data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
  84. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
  85. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
  86. data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
  87. data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
  88. data/lib/ruby_llm/agents/image/editor.rb +0 -1
  89. data/lib/ruby_llm/agents/image/generator.rb +0 -21
  90. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
  91. data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
  92. data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
  93. data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
  94. data/lib/ruby_llm/agents/image/transformer.rb +0 -1
  95. data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
  96. data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
  97. data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
  98. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  99. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  100. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  101. data/lib/ruby_llm/agents/infrastructure/execution_logger_job.rb +8 -0
  102. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  103. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  104. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  105. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  106. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  107. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  108. data/lib/ruby_llm/agents/results/base.rb +1 -49
  109. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  110. data/lib/ruby_llm/agents.rb +1 -9
  111. data/lib/tasks/ruby_llm_agents.rake +34 -0
  112. metadata +14 -83
  113. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  114. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  115. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  116. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  117. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  118. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  119. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  120. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  121. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  122. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  123. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  125. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  126. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  127. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  128. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  129. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  130. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  131. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  132. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  133. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  134. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  135. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  136. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  137. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  138. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  139. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  140. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  141. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  142. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  143. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  144. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  145. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  146. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  147. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  148. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  149. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  150. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  151. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  152. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  153. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  154. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  155. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  156. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  157. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  158. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  159. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  160. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  161. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  162. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  163. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  164. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  165. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  166. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  167. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  168. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  169. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  170. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  171. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  172. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  173. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  174. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  175. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  176. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  177. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  178. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  180. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  181. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  182. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  183. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  185. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  186. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  187. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  188. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  189. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  190. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  191. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -21,12 +21,6 @@ module RubyLLM
21
21
  #
22
22
  # @api public
23
23
  class AgentRegistry
24
- # Base workflow classes to exclude from listings
25
- # These are abstract parent classes, not concrete workflows
26
- BASE_WORKFLOW_CLASSES = [
27
- "RubyLLM::Agents::Workflow"
28
- ].freeze
29
-
30
24
  class << self
31
25
  # Returns all unique agent type names
32
26
  #
@@ -66,22 +60,17 @@ module RubyLLM
66
60
  #
67
61
  # @return [Array<String>] Agent class names
68
62
  def file_system_agents
69
- # Ensure all agent and workflow classes are loaded
63
+ # Ensure all agent classes are loaded
70
64
  eager_load_agents!
71
65
 
72
66
  # Find all descendants of all base classes
73
67
  agents = RubyLLM::Agents::Base.descendants.map(&:name).compact
74
- workflows = RubyLLM::Agents::Workflow.descendants.map(&:name).compact
75
68
  embedders = RubyLLM::Agents::Embedder.descendants.map(&:name).compact
76
- moderators = RubyLLM::Agents::Moderator.descendants.map(&:name).compact
77
69
  speakers = RubyLLM::Agents::Speaker.descendants.map(&:name).compact
78
70
  transcribers = RubyLLM::Agents::Transcriber.descendants.map(&:name).compact
79
71
  image_generators = RubyLLM::Agents::ImageGenerator.descendants.map(&:name).compact
80
72
 
81
- all_agents = (agents + workflows + embedders + moderators + speakers + transcribers + image_generators).uniq
82
-
83
- # Filter out base workflow classes
84
- all_agents.reject { |name| BASE_WORKFLOW_CLASSES.include?(name) }
73
+ (agents + embedders + speakers + transcribers + image_generators).uniq
85
74
  rescue StandardError => e
86
75
  Rails.logger.error("[RubyLLM::Agents] Error loading agents from file system: #{e.message}")
87
76
  []
@@ -97,7 +86,7 @@ module RubyLLM
97
86
  []
98
87
  end
99
88
 
100
- # Eager loads all agent and workflow files to register descendants
89
+ # Eager loads all agent files to register descendants
101
90
  #
102
91
  # Uses the configured autoload paths from RubyLLM::Agents.configuration
103
92
  # to ensure agents are discovered in the correct directories.
@@ -116,28 +105,6 @@ module RubyLLM
116
105
  end
117
106
  end
118
107
 
119
- # Returns only regular agents (non-workflows)
120
- #
121
- # @return [Array<Hash>] Agent info hashes for non-workflow agents
122
- def agents_only
123
- all_with_details.reject { |a| a[:is_workflow] }
124
- end
125
-
126
- # Returns only workflows
127
- #
128
- # @return [Array<Hash>] Agent info hashes for workflows only
129
- def workflows_only
130
- all_with_details.select { |a| a[:is_workflow] }
131
- end
132
-
133
- # Returns workflows filtered by type
134
- #
135
- # @param type [String, Symbol] The workflow type (pipeline, parallel, router)
136
- # @return [Array<Hash>] Filtered workflow info hashes
137
- def workflows_by_type(type)
138
- workflows_only.select { |w| w[:workflow_type] == type.to_s }
139
- end
140
-
141
108
  # Builds detailed info hash for an agent
142
109
  #
143
110
  # @param agent_type [String] The agent class name
@@ -146,27 +113,17 @@ module RubyLLM
146
113
  agent_class = find(agent_type)
147
114
  stats = fetch_stats(agent_type)
148
115
 
149
- # Detect the agent type (agent, workflow, embedder, moderator, speaker, transcriber)
116
+ # Detect the agent type (agent, embedder, speaker, transcriber, image_generator)
150
117
  detected_type = detect_agent_type(agent_class)
151
118
 
152
- # Check if this is a workflow class vs a regular agent
153
- is_workflow = detected_type == "workflow"
154
-
155
- # Determine specific workflow type and children
156
- workflow_type = is_workflow ? detect_workflow_type(agent_class) : nil
157
- workflow_children = is_workflow ? extract_workflow_children(agent_class) : []
158
-
159
119
  {
160
120
  name: agent_type,
161
121
  class: agent_class,
162
122
  active: agent_class.present?,
163
123
  agent_type: detected_type,
164
- is_workflow: is_workflow,
165
- workflow_type: workflow_type,
166
- workflow_children: workflow_children,
167
124
  version: safe_call(agent_class, :version) || "N/A",
168
125
  description: safe_call(agent_class, :description),
169
- model: safe_call(agent_class, :model) || (is_workflow ? "workflow" : "N/A"),
126
+ model: safe_call(agent_class, :model) || "N/A",
170
127
  temperature: safe_call(agent_class, :temperature),
171
128
  timeout: safe_call(agent_class, :timeout),
172
129
  cache_enabled: safe_call(agent_class, :cache_enabled?) || false,
@@ -216,22 +173,10 @@ module RubyLLM
216
173
  nil
217
174
  end
218
175
 
219
- # Detects the specific workflow type from class hierarchy
220
- #
221
- # @param agent_class [Class, nil] The agent class
222
- # @return [String, nil] "workflow" for DSL workflows, or nil
223
- def detect_workflow_type(agent_class)
224
- return nil unless agent_class
225
-
226
- if agent_class.respond_to?(:step_configs) && agent_class.step_configs.any?
227
- "workflow"
228
- end
229
- end
230
-
231
176
  # Detects the agent type from class hierarchy
232
177
  #
233
178
  # @param agent_class [Class, nil] The agent class
234
- # @return [String] "agent", "workflow", "embedder", "moderator", "speaker", "transcriber", or "image_generator"
179
+ # @return [String] "agent", "embedder", "speaker", "transcriber", or "image_generator"
235
180
  def detect_agent_type(agent_class)
236
181
  return "agent" unless agent_class
237
182
 
@@ -239,67 +184,16 @@ module RubyLLM
239
184
 
240
185
  if ancestors.include?("RubyLLM::Agents::Embedder")
241
186
  "embedder"
242
- elsif ancestors.include?("RubyLLM::Agents::Moderator")
243
- "moderator"
244
187
  elsif ancestors.include?("RubyLLM::Agents::Speaker")
245
188
  "speaker"
246
189
  elsif ancestors.include?("RubyLLM::Agents::Transcriber")
247
190
  "transcriber"
248
191
  elsif ancestors.include?("RubyLLM::Agents::ImageGenerator")
249
192
  "image_generator"
250
- elsif ancestors.include?("RubyLLM::Agents::Workflow")
251
- "workflow"
252
193
  else
253
194
  "agent"
254
195
  end
255
196
  end
256
-
257
- # Extracts child agents from workflow DSL configuration
258
- #
259
- # @param agent_class [Class, nil] The workflow class
260
- # @return [Array<Hash>] Array of child info hashes with :name, :agent, :type, :optional keys
261
- def extract_workflow_children(agent_class)
262
- return [] unless agent_class
263
-
264
- children = []
265
-
266
- if agent_class.respond_to?(:steps) && agent_class.steps.any?
267
- # Pipeline workflow - extract steps
268
- agent_class.steps.each do |name, config|
269
- children << {
270
- name: name,
271
- agent: config[:agent]&.name,
272
- type: "step",
273
- optional: config[:continue_on_error] || false
274
- }
275
- end
276
- elsif agent_class.respond_to?(:branches) && agent_class.branches.any?
277
- # Parallel workflow - extract branches
278
- agent_class.branches.each do |name, config|
279
- children << {
280
- name: name,
281
- agent: config[:agent]&.name,
282
- type: "branch",
283
- optional: config[:optional] || false
284
- }
285
- end
286
- elsif agent_class.respond_to?(:routes) && agent_class.routes.any?
287
- # Router workflow - extract routes
288
- agent_class.routes.each do |name, config|
289
- children << {
290
- name: name,
291
- agent: config[:agent]&.name,
292
- type: "route",
293
- description: config[:description]
294
- }
295
- end
296
- end
297
-
298
- children
299
- rescue StandardError => e
300
- Rails.logger.error("[RubyLLM::Agents] Error extracting workflow children: #{e.message}")
301
- []
302
- end
303
197
  end
304
198
  end
305
199
  end
@@ -37,26 +37,26 @@
37
37
  credits: { enabled: false },
38
38
  chart: {
39
39
  backgroundColor: 'transparent',
40
- style: { fontFamily: 'inherit' }
40
+ style: { fontFamily: 'ui-monospace, monospace' }
41
41
  },
42
42
  title: { text: null },
43
43
  xAxis: {
44
- labels: { style: { color: '#9CA3AF' } },
45
- lineColor: 'rgba(156, 163, 175, 0.2)',
46
- tickColor: 'rgba(156, 163, 175, 0.2)'
44
+ labels: { style: { color: '#6B7280' } },
45
+ lineColor: 'rgba(107, 114, 128, 0.1)',
46
+ tickColor: 'rgba(107, 114, 128, 0.1)'
47
47
  },
48
48
  yAxis: {
49
- labels: { style: { color: '#9CA3AF' } },
50
- gridLineColor: 'rgba(156, 163, 175, 0.2)'
49
+ labels: { style: { color: '#6B7280' } },
50
+ gridLineColor: 'rgba(107, 114, 128, 0.1)'
51
51
  },
52
52
  legend: {
53
- itemStyle: { color: '#9CA3AF' },
54
- itemHoverStyle: { color: '#D1D5DB' }
53
+ itemStyle: { color: '#6B7280' },
54
+ itemHoverStyle: { color: '#9CA3AF' }
55
55
  },
56
56
  tooltip: {
57
- backgroundColor: 'rgba(17, 24, 39, 0.9)',
58
- borderColor: 'rgba(75, 85, 99, 0.5)',
59
- style: { color: '#F3F4F6' }
57
+ backgroundColor: 'rgba(0, 0, 0, 0.85)',
58
+ borderColor: 'transparent',
59
+ style: { color: '#E5E7EB', fontFamily: 'ui-monospace, monospace' }
60
60
  }
61
61
  });
62
62
  </script>
@@ -71,12 +71,9 @@
71
71
 
72
72
  <!-- Clickable rows -->
73
73
  <script type="module">
74
- // Initialize on page load
75
74
  document.addEventListener('DOMContentLoaded', function() {
76
- // Handle data-href clickable rows (semantic alternative to onclick)
77
75
  document.querySelectorAll('[data-href]').forEach(function(element) {
78
76
  element.addEventListener('click', function(e) {
79
- // Don't navigate if clicking on a link or button inside the row
80
77
  if (e.target.closest('a, button')) return;
81
78
  window.location = element.dataset.href;
82
79
  });
@@ -109,291 +106,131 @@
109
106
  }
110
107
  </script>
111
108
 
112
- <style>
113
- /* Custom styles for the dashboard */
114
-
115
- /* Standard card components */
116
- .card {
117
- @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6;
118
- }
119
- .card-compact {
120
- @apply bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-4 mb-4;
121
- }
122
-
123
- .stat-card {
124
- @apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-4 transition-shadow;
125
- }
126
- .stat-card:hover {
127
- @apply shadow-md;
128
- }
129
- .stat-value {
130
- @apply text-2xl font-bold text-gray-900 dark:text-gray-100 mt-2;
131
- }
132
- @media (min-width: 1024px) {
133
- .stat-value {
134
- font-size: 1.5rem;
135
- }
136
- }
137
- .stat-label {
138
- @apply text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wide font-medium;
139
- }
140
- .chart-card {
141
- @apply bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 p-6;
142
- }
143
- .nav-link {
144
- @apply px-4 py-2 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 rounded-md transition-colors;
145
- }
146
- .nav-link.active {
147
- @apply bg-blue-50 dark:bg-blue-900 text-blue-700 dark:text-blue-300;
148
- }
109
+ <style type="text/tailwindcss">
149
110
  .badge {
150
111
  @apply inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium;
151
112
  }
152
113
  .badge-running {
153
- @apply bg-blue-100 dark:bg-blue-900 text-blue-800 dark:text-blue-200;
114
+ @apply bg-blue-100 dark:bg-blue-500/20 text-blue-700 dark:text-blue-300;
154
115
  }
155
116
  .badge-success {
156
- @apply bg-green-100 dark:bg-green-900 text-green-800 dark:text-green-200;
117
+ @apply bg-green-100 dark:bg-green-500/20 text-green-700 dark:text-green-300;
157
118
  }
158
119
  .badge-error {
159
- @apply bg-red-100 dark:bg-red-900 text-red-800 dark:text-red-200;
120
+ @apply bg-red-100 dark:bg-red-500/20 text-red-700 dark:text-red-300;
160
121
  }
161
122
  .badge-timeout {
162
- @apply bg-yellow-100 dark:bg-yellow-900 text-yellow-800 dark:text-yellow-200;
123
+ @apply bg-yellow-100 dark:bg-yellow-500/20 text-yellow-700 dark:text-yellow-300;
163
124
  }
164
125
  .badge-cyan {
165
- @apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
126
+ @apply bg-cyan-100 dark:bg-cyan-500/20 text-cyan-700 dark:text-cyan-300;
166
127
  }
167
128
  .badge-purple {
168
- @apply bg-purple-100 dark:bg-purple-900/50 text-purple-700 dark:text-purple-300;
129
+ @apply bg-purple-100 dark:bg-purple-500/20 text-purple-700 dark:text-purple-300;
169
130
  }
170
131
  .badge-orange {
171
- @apply bg-orange-100 dark:bg-orange-900/50 text-orange-700 dark:text-orange-300;
132
+ @apply bg-orange-100 dark:bg-orange-500/20 text-orange-700 dark:text-orange-300;
172
133
  }
173
-
174
- /* Workflow type badges */
175
- .badge-pipeline {
176
- @apply bg-indigo-100 dark:bg-indigo-900/50 text-indigo-700 dark:text-indigo-300;
177
- }
178
- .badge-parallel {
179
- @apply bg-cyan-100 dark:bg-cyan-900/50 text-cyan-700 dark:text-cyan-300;
180
- }
181
- .badge-router {
182
- @apply bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300;
183
- }
184
-
185
- /* Alpine.js utilities */
186
- [x-cloak] {
187
- display: none !important;
134
+ .badge-sm {
135
+ @apply inline-flex items-center px-1.5 py-px rounded-full text-[10px] font-medium;
188
136
  }
189
-
190
- </style>
137
+ </style>
191
138
  </head>
192
139
 
193
- <body class="bg-gray-50 dark:bg-gray-900 min-h-screen flex flex-col">
140
+ <body class="bg-gray-50 dark:bg-gray-950 min-h-screen flex flex-col font-sans">
194
141
  <!-- Header -->
195
- <header
196
- class="
197
- bg-white dark:bg-gray-800 border-b border-gray-200
198
- dark:border-gray-700
199
- "
200
- x-data="{ mobileMenuOpen: false }"
201
- >
202
- <div class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 h-16 flex items-center">
203
- <div class="flex justify-between items-center w-full">
204
- <div class="flex items-center space-x-8">
205
- <%= link_to ruby_llm_agents.root_path, class: "flex items-center space-x-2" do %>
206
- <span class="text-lg">🤖</span>
207
-
208
- <span class="font-semibold text-gray-900 dark:text-gray-100">
209
- RubyLLM Agents
210
- </span>
211
- <% end %>
142
+ <header class="border-b border-gray-200 dark:border-gray-800" x-data="{ mobileMenuOpen: false }">
143
+ <div class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 h-12 flex items-center">
144
+ <!-- Brand -->
145
+ <%= link_to ruby_llm_agents.root_path, class: "font-mono text-sm font-medium text-gray-900 dark:text-gray-100 hover:text-gray-600 dark:hover:text-gray-400 transition-colors" do %>
146
+ ruby_llm<span class="text-gray-400 dark:text-gray-600">::</span>agents
147
+ <% end %>
148
+
149
+ <!-- Desktop Navigation -->
150
+ <nav class="hidden md:flex items-center ml-8 gap-0.5 font-mono text-xs">
151
+ <% nav_items = [
152
+ [ruby_llm_agents.root_path, "dashboard"],
153
+ [ruby_llm_agents.agents_path, "agents"],
154
+ [ruby_llm_agents.executions_path, "executions"]
155
+ ]
156
+ nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
157
+ nav_items.each do |path, label| %>
158
+ <% active = current_page?(path) %>
159
+ <%= link_to label, path, class: "px-2.5 py-1 rounded transition-colors #{active ? 'text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-800' : 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}" %>
160
+ <% end %>
161
+ </nav>
212
162
 
213
- <!-- Desktop Navigation -->
214
- <%
215
- nav_items = [
216
- { 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" />' },
217
- { path: ruby_llm_agents.agents_path, label: "Agents", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>' },
218
- { path: ruby_llm_agents.workflows_path, label: "Workflows", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zm0 8a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zm12 0a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z"/>' },
219
- { 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" />' },
220
- { path: ruby_llm_agents.tenants_path, label: "Tenants", icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />' }
221
- ]
222
- %>
223
- <nav class="hidden md:flex items-center space-x-1">
224
- <% nav_items.each do |item| %>
225
- <%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: false %>
226
- <% end %>
227
- </nav>
228
- </div>
163
+ <!-- Right Side -->
164
+ <div class="ml-auto flex items-center gap-1">
165
+ <%= link_to ruby_llm_agents.system_config_path, class: "hidden md:flex p-1.5 text-gray-400 dark:text-gray-600 hover:text-gray-700 dark:hover:text-gray-300 rounded transition-colors", title: "Config" do %>
166
+ <svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
167
+ <path stroke-linecap="round" stroke-linejoin="round" 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"/>
168
+ <path stroke-linecap="round" stroke-linejoin="round" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
169
+ </svg>
170
+ <% end %>
229
171
 
230
- <div
231
- class="
232
- flex items-center space-x-2 text-xs text-gray-500
233
- dark:text-gray-400
234
- "
172
+ <!-- Mobile menu button -->
173
+ <button
174
+ type="button"
175
+ @click="mobileMenuOpen = !mobileMenuOpen"
176
+ :aria-expanded="mobileMenuOpen"
177
+ class="md:hidden p-1.5 text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 rounded transition-colors"
235
178
  >
236
- <%# Settings Dropdown %>
237
- <div x-data="{ settingsOpen: false }" class="relative hidden md:block">
238
- <button
239
- @click="settingsOpen = !settingsOpen"
240
- @click.outside="settingsOpen = false"
241
- type="button"
242
- class="p-2 rounded-md text-gray-500 hover:text-gray-700 hover:bg-gray-100 dark:text-gray-400 dark:hover:text-gray-200 dark:hover:bg-gray-700 transition-colors"
243
- aria-label="Settings"
244
- >
245
- <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
246
- <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"/>
247
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
248
- </svg>
249
- </button>
250
-
251
- <div
252
- x-show="settingsOpen"
253
- x-cloak
254
- x-transition:enter="transition ease-out duration-100"
255
- x-transition:enter-start="opacity-0 scale-95"
256
- x-transition:enter-end="opacity-100 scale-100"
257
- x-transition:leave="transition ease-in duration-75"
258
- x-transition:leave-start="opacity-100 scale-100"
259
- x-transition:leave-end="opacity-0 scale-95"
260
- class="absolute right-0 mt-2 w-48 bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-lg shadow-lg z-50 py-1"
261
- >
262
- <a href="<%= ruby_llm_agents.system_config_path %>"
263
- class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-gray-100 dark:bg-gray-700' if request.path == ruby_llm_agents.system_config_path %>">
264
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
265
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>
266
- </svg>
267
- System Config
268
- </a>
269
- <a href="<%= ruby_llm_agents.api_configuration_path %>"
270
- class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 <%= 'bg-gray-100 dark:bg-gray-700' if request.path == ruby_llm_agents.api_configuration_path %>">
271
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
272
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>
273
- </svg>
274
- API Keys
275
- </a>
276
- <div class="border-t border-gray-100 dark:border-gray-700 my-1"></div>
277
- <a href="#" class="flex items-center gap-2 px-4 py-2 text-sm text-gray-700 dark:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700">
278
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
279
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
280
- </svg>
281
- Logout
282
- </a>
283
- </div>
284
- </div>
285
-
286
- <!-- Mobile menu button -->
287
- <button
288
- type="button"
289
- @click="mobileMenuOpen = !mobileMenuOpen"
290
- :aria-expanded="mobileMenuOpen"
291
- aria-controls="mobile-menu"
292
- class="
293
- md:hidden inline-flex items-center justify-center p-2
294
- rounded-md text-gray-500 dark:text-gray-400
295
- hover:text-gray-900 dark:hover:text-gray-100
296
- hover:bg-gray-100 dark:hover:bg-gray-700 focus:outline-none
297
- focus:ring-2 focus:ring-inset focus:ring-blue-500
298
- "
299
- >
300
- <span class="sr-only">Open main menu</span>
301
-
302
- <svg
303
- class="w-5 h-5"
304
- fill="none"
305
- stroke="currentColor"
306
- viewBox="0 0 24 24"
307
- >
308
- <path
309
- stroke-linecap="round"
310
- stroke-linejoin="round"
311
- stroke-width="2"
312
- d="M4 6h16M4 12h16M4 18h16"
313
- />
314
- </svg>
315
- </button>
316
- </div>
179
+ <span class="sr-only">Open main menu</span>
180
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" stroke-width="2" viewBox="0 0 24 24">
181
+ <path stroke-linecap="round" stroke-linejoin="round" d="M4 6h16M4 12h16M4 18h16"/>
182
+ </svg>
183
+ </button>
317
184
  </div>
318
185
  </div>
319
186
 
320
- <!-- Mobile Navigation Menu -->
187
+ <!-- Mobile Navigation -->
321
188
  <div
322
- id="mobile-menu"
323
189
  x-show="mobileMenuOpen"
324
190
  x-cloak
325
- x-transition:enter="transition ease-out duration-200"
191
+ x-transition:enter="transition ease-out duration-150"
326
192
  x-transition:enter-start="opacity-0 -translate-y-1"
327
193
  x-transition:enter-end="opacity-100 translate-y-0"
328
- x-transition:leave="transition ease-in duration-150"
194
+ x-transition:leave="transition ease-in duration-100"
329
195
  x-transition:leave-start="opacity-100 translate-y-0"
330
196
  x-transition:leave-end="opacity-0 -translate-y-1"
331
197
  @click.outside="mobileMenuOpen = false"
332
- class="
333
- md:hidden border-t border-gray-200 dark:border-gray-700 bg-white
334
- dark:bg-gray-800
335
- "
198
+ class="md:hidden border-t border-gray-200 dark:border-gray-800"
336
199
  >
337
- <nav class="max-w-7xl mx-auto px-4 py-3 space-y-1">
338
- <% nav_items.each do |item| %>
339
- <%= render "ruby_llm/agents/shared/nav_link", path: item[:path], label: item[:label], icon: item[:icon], mobile: true %>
200
+ <nav class="max-w-7xl mx-auto px-4 py-2 font-mono text-xs space-y-0.5">
201
+ <% mobile_nav_items = [
202
+ [ruby_llm_agents.root_path, "dashboard"],
203
+ [ruby_llm_agents.agents_path, "agents"],
204
+ [ruby_llm_agents.executions_path, "executions"]
205
+ ]
206
+ mobile_nav_items << [ruby_llm_agents.tenants_path, "tenants"] if tenant_filter_enabled?
207
+ mobile_nav_items.each do |path, label| %>
208
+ <% active = current_page?(path) %>
209
+ <%= link_to label, path, class: "block px-3 py-2 rounded transition-colors #{active ? 'text-gray-900 dark:text-gray-100 bg-gray-100 dark:bg-gray-800' : 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}" %>
340
210
  <% end %>
341
-
342
- <%# Settings section divider %>
343
- <div class="border-t border-gray-200 dark:border-gray-700 my-2 pt-2">
344
- <span class="px-3 text-xs font-semibold text-gray-400 dark:text-gray-500 uppercase tracking-wider">Settings</span>
345
- </div>
346
-
347
- <%= render "ruby_llm/agents/shared/nav_link",
348
- path: ruby_llm_agents.system_config_path,
349
- label: "System Config",
350
- icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"/>',
351
- mobile: true %>
352
-
353
- <%= render "ruby_llm/agents/shared/nav_link",
354
- path: ruby_llm_agents.api_configuration_path,
355
- label: "API Keys",
356
- icon: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 7a2 2 0 012 2m4 0a6 6 0 01-7.743 5.743L11 17H9v2H7v2H4a1 1 0 01-1-1v-2.586a1 1 0 01.293-.707l5.964-5.964A6 6 0 1121 9z"/>',
357
- mobile: true %>
358
-
359
- <a href="#" class="flex items-center px-3 py-2 text-sm font-medium 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 rounded-md">
360
- <svg class="w-5 h-5 mr-3 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
361
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M17 16l4-4m0 0l-4-4m4 4H7m6 4v1a3 3 0 01-3 3H6a3 3 0 01-3-3V7a3 3 0 013-3h4a3 3 0 013 3v1"/>
362
- </svg>
363
- Logout
364
- </a>
211
+ <div class="border-t border-gray-200 dark:border-gray-800 my-1.5"></div>
212
+ <%= link_to "config", ruby_llm_agents.system_config_path, class: "block px-3 py-2 rounded text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300 transition-colors" %>
365
213
  </nav>
366
214
  </div>
367
215
  </header>
368
216
 
369
217
  <!-- Main content -->
370
- <main class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-8 h-full">
218
+ <main class="max-w-7xl w-full mx-auto px-4 sm:px-6 lg:px-8 py-6 flex-1">
371
219
  <%= yield %>
372
220
  </main>
373
221
 
374
222
  <!-- Footer -->
375
- <footer
376
- class="border-t bg-white dark:bg-gray-800 dark:border-gray-700 mt-auto"
377
- x-data="themeManager()"
378
- >
379
- <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-4">
380
- <div class="flex flex-col sm:flex-row items-center sm:justify-between gap-3 sm:gap-0">
381
- <div class="flex items-center space-x-2">
382
- <label for="theme-select" class="text-sm text-gray-500 dark:text-gray-400">Theme:</label>
383
- <select
384
- id="theme-select"
385
- x-model="preference"
386
- @change="setTheme($event.target.value)"
387
- 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"
388
- >
389
- <option value="light">Light</option>
390
- <option value="dark">Dark</option>
391
- <option value="auto">Auto</option>
392
- </select>
223
+ <footer class="border-t border-gray-200 dark:border-gray-800 mt-auto" x-data="themeManager()">
224
+ <div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-3">
225
+ <div class="flex items-center justify-between font-mono text-[10px] text-gray-400 dark:text-gray-600 uppercase tracking-widest">
226
+ <div class="flex items-center gap-1">
227
+ <button @click="setTheme('light')" :class="preference === 'light' && 'text-gray-700 dark:text-gray-300'" class="px-1.5 py-0.5 rounded hover:text-gray-600 dark:hover:text-gray-400 transition-colors">light</button>
228
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
229
+ <button @click="setTheme('dark')" :class="preference === 'dark' && 'text-gray-700 dark:text-gray-300'" class="px-1.5 py-0.5 rounded hover:text-gray-600 dark:hover:text-gray-400 transition-colors">dark</button>
230
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
231
+ <button @click="setTheme('auto')" :class="preference === 'auto' && 'text-gray-700 dark:text-gray-300'" class="px-1.5 py-0.5 rounded hover:text-gray-600 dark:hover:text-gray-400 transition-colors">auto</button>
393
232
  </div>
394
- <p class="text-sm text-gray-500 dark:text-gray-400 text-center">
395
- 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>
396
- </p>
233
+ <a href="https://github.com/adham90/ruby_llm-agents" class="hover:text-gray-600 dark:hover:text-gray-400 transition-colors normal-case tracking-normal">ruby_llm-agents</a>
397
234
  </div>
398
235
  </div>
399
236
  </footer>