ruby_llm-agents 0.4.0 → 1.0.0.beta.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 (208) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +225 -34
  3. data/app/controllers/ruby_llm/agents/agents_controller.rb +136 -16
  4. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +214 -0
  5. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +29 -9
  6. data/app/controllers/ruby_llm/agents/{settings_controller.rb → system_config_controller.rb} +3 -3
  7. data/app/controllers/ruby_llm/agents/tenants_controller.rb +109 -0
  8. data/app/controllers/ruby_llm/agents/workflows_controller.rb +355 -0
  9. data/app/helpers/ruby_llm/agents/application_helper.rb +25 -0
  10. data/app/models/ruby_llm/agents/api_configuration.rb +386 -0
  11. data/app/models/ruby_llm/agents/execution.rb +3 -0
  12. data/app/models/ruby_llm/agents/tenant_budget.rb +112 -14
  13. data/app/services/ruby_llm/agents/agent_registry.rb +51 -12
  14. data/app/views/layouts/ruby_llm/agents/application.html.erb +5 -30
  15. data/app/views/ruby_llm/agents/agents/_agent.html.erb +13 -1
  16. data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +235 -0
  17. data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +70 -0
  18. data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +152 -0
  19. data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +63 -0
  20. data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +108 -0
  21. data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +91 -0
  22. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +1 -1
  23. data/app/views/ruby_llm/agents/agents/index.html.erb +74 -9
  24. data/app/views/ruby_llm/agents/agents/show.html.erb +18 -378
  25. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +34 -0
  26. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +288 -0
  27. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +95 -0
  28. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +97 -0
  29. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +211 -0
  30. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +179 -0
  31. data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +1 -1
  32. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +269 -15
  33. data/app/views/ruby_llm/agents/executions/show.html.erb +98 -0
  34. data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +93 -0
  35. data/app/views/ruby_llm/agents/{settings → system_config}/show.html.erb +1 -1
  36. data/app/views/ruby_llm/agents/tenants/_form.html.erb +150 -0
  37. data/app/views/ruby_llm/agents/tenants/edit.html.erb +13 -0
  38. data/app/views/ruby_llm/agents/tenants/index.html.erb +129 -0
  39. data/app/views/ruby_llm/agents/tenants/show.html.erb +374 -0
  40. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +236 -0
  41. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +76 -0
  42. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +74 -0
  43. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +108 -0
  44. data/app/views/ruby_llm/agents/workflows/show.html.erb +442 -0
  45. data/config/routes.rb +13 -1
  46. data/lib/generators/ruby_llm_agents/agent_generator.rb +56 -7
  47. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +100 -0
  48. data/lib/generators/ruby_llm_agents/background_remover_generator.rb +110 -0
  49. data/lib/generators/ruby_llm_agents/embedder_generator.rb +107 -0
  50. data/lib/generators/ruby_llm_agents/image_analyzer_generator.rb +115 -0
  51. data/lib/generators/ruby_llm_agents/image_editor_generator.rb +108 -0
  52. data/lib/generators/ruby_llm_agents/image_generator_generator.rb +116 -0
  53. data/lib/generators/ruby_llm_agents/image_pipeline_generator.rb +178 -0
  54. data/lib/generators/ruby_llm_agents/image_transformer_generator.rb +109 -0
  55. data/lib/generators/ruby_llm_agents/image_upscaler_generator.rb +103 -0
  56. data/lib/generators/ruby_llm_agents/image_variator_generator.rb +102 -0
  57. data/lib/generators/ruby_llm_agents/install_generator.rb +76 -4
  58. data/lib/generators/ruby_llm_agents/restructure_generator.rb +292 -0
  59. data/lib/generators/ruby_llm_agents/speaker_generator.rb +121 -0
  60. data/lib/generators/ruby_llm_agents/templates/add_execution_type_migration.rb.tt +8 -0
  61. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +99 -84
  62. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +42 -40
  63. data/lib/generators/ruby_llm_agents/templates/application_background_remover.rb.tt +26 -0
  64. data/lib/generators/ruby_llm_agents/templates/application_embedder.rb.tt +50 -0
  65. data/lib/generators/ruby_llm_agents/templates/application_image_analyzer.rb.tt +26 -0
  66. data/lib/generators/ruby_llm_agents/templates/application_image_editor.rb.tt +20 -0
  67. data/lib/generators/ruby_llm_agents/templates/application_image_generator.rb.tt +38 -0
  68. data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +139 -0
  69. data/lib/generators/ruby_llm_agents/templates/application_image_transformer.rb.tt +21 -0
  70. data/lib/generators/ruby_llm_agents/templates/application_image_upscaler.rb.tt +20 -0
  71. data/lib/generators/ruby_llm_agents/templates/application_image_variator.rb.tt +20 -0
  72. data/lib/generators/ruby_llm_agents/templates/application_speaker.rb.tt +49 -0
  73. data/lib/generators/ruby_llm_agents/templates/application_transcriber.rb.tt +53 -0
  74. data/lib/generators/ruby_llm_agents/templates/background_remover.rb.tt +44 -0
  75. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +90 -0
  76. data/lib/generators/ruby_llm_agents/templates/embedder.rb.tt +41 -0
  77. data/lib/generators/ruby_llm_agents/templates/image_analyzer.rb.tt +45 -0
  78. data/lib/generators/ruby_llm_agents/templates/image_editor.rb.tt +35 -0
  79. data/lib/generators/ruby_llm_agents/templates/image_generator.rb.tt +47 -0
  80. data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +50 -0
  81. data/lib/generators/ruby_llm_agents/templates/image_transformer.rb.tt +44 -0
  82. data/lib/generators/ruby_llm_agents/templates/image_upscaler.rb.tt +38 -0
  83. data/lib/generators/ruby_llm_agents/templates/image_variator.rb.tt +33 -0
  84. data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +228 -0
  85. data/lib/generators/ruby_llm_agents/templates/skills/BACKGROUND_REMOVERS.md.tt +131 -0
  86. data/lib/generators/ruby_llm_agents/templates/skills/EMBEDDERS.md.tt +255 -0
  87. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_ANALYZERS.md.tt +120 -0
  88. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_EDITORS.md.tt +102 -0
  89. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_GENERATORS.md.tt +282 -0
  90. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +228 -0
  91. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_TRANSFORMERS.md.tt +120 -0
  92. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_UPSCALERS.md.tt +110 -0
  93. data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_VARIATORS.md.tt +120 -0
  94. data/lib/generators/ruby_llm_agents/templates/skills/SPEAKERS.md.tt +212 -0
  95. data/lib/generators/ruby_llm_agents/templates/skills/TOOLS.md.tt +227 -0
  96. data/lib/generators/ruby_llm_agents/templates/skills/TRANSCRIBERS.md.tt +251 -0
  97. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +300 -0
  98. data/lib/generators/ruby_llm_agents/templates/speaker.rb.tt +56 -0
  99. data/lib/generators/ruby_llm_agents/templates/transcriber.rb.tt +51 -0
  100. data/lib/generators/ruby_llm_agents/transcriber_generator.rb +107 -0
  101. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +152 -1
  102. data/lib/ruby_llm/agents/audio/speaker.rb +553 -0
  103. data/lib/ruby_llm/agents/audio/transcriber.rb +669 -0
  104. data/lib/ruby_llm/agents/base_agent.rb +675 -0
  105. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +181 -0
  106. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +274 -0
  107. data/lib/ruby_llm/agents/core/base.rb +135 -0
  108. data/lib/ruby_llm/agents/core/configuration.rb +981 -0
  109. data/lib/ruby_llm/agents/core/errors.rb +150 -0
  110. data/lib/ruby_llm/agents/{instrumentation.rb → core/instrumentation.rb} +93 -4
  111. data/lib/ruby_llm/agents/core/llm_tenant.rb +358 -0
  112. data/lib/ruby_llm/agents/core/resolved_config.rb +348 -0
  113. data/lib/ruby_llm/agents/{version.rb → core/version.rb} +1 -1
  114. data/lib/ruby_llm/agents/dsl/base.rb +110 -0
  115. data/lib/ruby_llm/agents/dsl/caching.rb +142 -0
  116. data/lib/ruby_llm/agents/dsl/reliability.rb +307 -0
  117. data/lib/ruby_llm/agents/dsl.rb +41 -0
  118. data/lib/ruby_llm/agents/image/analyzer/dsl.rb +130 -0
  119. data/lib/ruby_llm/agents/image/analyzer/execution.rb +402 -0
  120. data/lib/ruby_llm/agents/image/analyzer.rb +90 -0
  121. data/lib/ruby_llm/agents/image/background_remover/dsl.rb +154 -0
  122. data/lib/ruby_llm/agents/image/background_remover/execution.rb +240 -0
  123. data/lib/ruby_llm/agents/image/background_remover.rb +89 -0
  124. data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +91 -0
  125. data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +165 -0
  126. data/lib/ruby_llm/agents/image/editor/dsl.rb +56 -0
  127. data/lib/ruby_llm/agents/image/editor/execution.rb +207 -0
  128. data/lib/ruby_llm/agents/image/editor.rb +92 -0
  129. data/lib/ruby_llm/agents/image/generator/active_storage_support.rb +127 -0
  130. data/lib/ruby_llm/agents/image/generator/content_policy.rb +95 -0
  131. data/lib/ruby_llm/agents/image/generator/pricing.rb +353 -0
  132. data/lib/ruby_llm/agents/image/generator/templates.rb +124 -0
  133. data/lib/ruby_llm/agents/image/generator.rb +455 -0
  134. data/lib/ruby_llm/agents/image/pipeline/dsl.rb +213 -0
  135. data/lib/ruby_llm/agents/image/pipeline/execution.rb +382 -0
  136. data/lib/ruby_llm/agents/image/pipeline.rb +97 -0
  137. data/lib/ruby_llm/agents/image/transformer/dsl.rb +148 -0
  138. data/lib/ruby_llm/agents/image/transformer/execution.rb +223 -0
  139. data/lib/ruby_llm/agents/image/transformer.rb +95 -0
  140. data/lib/ruby_llm/agents/image/upscaler/dsl.rb +83 -0
  141. data/lib/ruby_llm/agents/image/upscaler/execution.rb +219 -0
  142. data/lib/ruby_llm/agents/image/upscaler.rb +81 -0
  143. data/lib/ruby_llm/agents/image/variator/dsl.rb +62 -0
  144. data/lib/ruby_llm/agents/image/variator/execution.rb +189 -0
  145. data/lib/ruby_llm/agents/image/variator.rb +80 -0
  146. data/lib/ruby_llm/agents/{alert_manager.rb → infrastructure/alert_manager.rb} +17 -22
  147. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +145 -0
  148. data/lib/ruby_llm/agents/infrastructure/budget/config_resolver.rb +149 -0
  149. data/lib/ruby_llm/agents/infrastructure/budget/forecaster.rb +68 -0
  150. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +279 -0
  151. data/lib/ruby_llm/agents/infrastructure/budget_tracker.rb +275 -0
  152. data/lib/ruby_llm/agents/{execution_logger_job.rb → infrastructure/execution_logger_job.rb} +17 -1
  153. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/executor.rb +2 -1
  154. data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/retry_strategy.rb +9 -3
  155. data/lib/ruby_llm/agents/{reliability.rb → infrastructure/reliability.rb} +11 -21
  156. data/lib/ruby_llm/agents/pipeline/builder.rb +215 -0
  157. data/lib/ruby_llm/agents/pipeline/context.rb +255 -0
  158. data/lib/ruby_llm/agents/pipeline/executor.rb +86 -0
  159. data/lib/ruby_llm/agents/pipeline/middleware/base.rb +124 -0
  160. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +95 -0
  161. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +171 -0
  162. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +415 -0
  163. data/lib/ruby_llm/agents/pipeline/middleware/reliability.rb +276 -0
  164. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +196 -0
  165. data/lib/ruby_llm/agents/pipeline.rb +68 -0
  166. data/lib/ruby_llm/agents/{engine.rb → rails/engine.rb} +79 -10
  167. data/lib/ruby_llm/agents/results/background_removal_result.rb +286 -0
  168. data/lib/ruby_llm/agents/{result.rb → results/base.rb} +73 -1
  169. data/lib/ruby_llm/agents/results/embedding_result.rb +243 -0
  170. data/lib/ruby_llm/agents/results/image_analysis_result.rb +314 -0
  171. data/lib/ruby_llm/agents/results/image_edit_result.rb +250 -0
  172. data/lib/ruby_llm/agents/results/image_generation_result.rb +346 -0
  173. data/lib/ruby_llm/agents/results/image_pipeline_result.rb +399 -0
  174. data/lib/ruby_llm/agents/results/image_transform_result.rb +251 -0
  175. data/lib/ruby_llm/agents/results/image_upscale_result.rb +255 -0
  176. data/lib/ruby_llm/agents/results/image_variation_result.rb +237 -0
  177. data/lib/ruby_llm/agents/results/moderation_result.rb +158 -0
  178. data/lib/ruby_llm/agents/results/speech_result.rb +338 -0
  179. data/lib/ruby_llm/agents/results/transcription_result.rb +408 -0
  180. data/lib/ruby_llm/agents/text/embedder.rb +444 -0
  181. data/lib/ruby_llm/agents/text/moderator.rb +237 -0
  182. data/lib/ruby_llm/agents/workflow/async.rb +220 -0
  183. data/lib/ruby_llm/agents/workflow/async_executor.rb +156 -0
  184. data/lib/ruby_llm/agents/{workflow.rb → workflow/orchestrator.rb} +6 -5
  185. data/lib/ruby_llm/agents/workflow/parallel.rb +34 -17
  186. data/lib/ruby_llm/agents/workflow/thread_pool.rb +185 -0
  187. data/lib/ruby_llm/agents.rb +86 -20
  188. metadata +189 -35
  189. data/lib/ruby_llm/agents/base/caching.rb +0 -40
  190. data/lib/ruby_llm/agents/base/cost_calculation.rb +0 -105
  191. data/lib/ruby_llm/agents/base/dsl.rb +0 -324
  192. data/lib/ruby_llm/agents/base/execution.rb +0 -283
  193. data/lib/ruby_llm/agents/base/reliability_dsl.rb +0 -82
  194. data/lib/ruby_llm/agents/base/reliability_execution.rb +0 -136
  195. data/lib/ruby_llm/agents/base/response_building.rb +0 -86
  196. data/lib/ruby_llm/agents/base/tool_tracking.rb +0 -57
  197. data/lib/ruby_llm/agents/base.rb +0 -209
  198. data/lib/ruby_llm/agents/budget_tracker.rb +0 -471
  199. data/lib/ruby_llm/agents/configuration.rb +0 -357
  200. /data/lib/ruby_llm/agents/{deprecations.rb → core/deprecations.rb} +0 -0
  201. /data/lib/ruby_llm/agents/{inflections.rb → core/inflections.rb} +0 -0
  202. /data/lib/ruby_llm/agents/{attempt_tracker.rb → infrastructure/attempt_tracker.rb} +0 -0
  203. /data/lib/ruby_llm/agents/{cache_helper.rb → infrastructure/cache_helper.rb} +0 -0
  204. /data/lib/ruby_llm/agents/{circuit_breaker.rb → infrastructure/circuit_breaker.rb} +0 -0
  205. /data/lib/ruby_llm/agents/{redactor.rb → infrastructure/redactor.rb} +0 -0
  206. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/breaker_manager.rb +0 -0
  207. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/execution_constraints.rb +0 -0
  208. /data/lib/ruby_llm/agents/{reliability → infrastructure/reliability}/fallback_routing.rb +0 -0
@@ -0,0 +1,288 @@
1
+ <% # Shared form partial for API configuration %>
2
+ <% # Local variables: f (form builder), config (ApiConfiguration instance), resolved (ResolvedConfig, optional) %>
3
+ <% resolved = local_assigns[:resolved] %>
4
+
5
+ <% if config.errors.any? %>
6
+ <div class="bg-red-50 dark:bg-red-900/20 rounded-lg p-4 border border-red-200 dark:border-red-800 mb-6">
7
+ <div class="flex">
8
+ <svg class="w-5 h-5 text-red-400 mr-3 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
9
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
10
+ </svg>
11
+ <div>
12
+ <h4 class="text-sm font-medium text-red-800 dark:text-red-200">Please fix the following errors:</h4>
13
+ <ul class="mt-2 text-sm text-red-700 dark:text-red-300 list-disc list-inside">
14
+ <% config.errors.full_messages.each do |message| %>
15
+ <li><%= message %></li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
19
+ </div>
20
+ </div>
21
+ <% end %>
22
+
23
+ <div class="space-y-6">
24
+ <!-- Primary Providers -->
25
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
26
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Primary Providers</h3>
27
+ <div class="space-y-4">
28
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :openai_api_key, label: "OpenAI API Key", placeholder: "sk-..." %>
29
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :anthropic_api_key, label: "Anthropic API Key", placeholder: "sk-ant-..." %>
30
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :gemini_api_key, label: "Google Gemini API Key", placeholder: "AI..." %>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Additional Providers -->
35
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
36
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Additional Providers</h3>
37
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
38
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :deepseek_api_key, label: "DeepSeek API Key" %>
39
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :mistral_api_key, label: "Mistral API Key" %>
40
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :perplexity_api_key, label: "Perplexity API Key" %>
41
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :openrouter_api_key, label: "OpenRouter API Key" %>
42
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :gpustack_api_key, label: "GPUStack API Key" %>
43
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :xai_api_key, label: "xAI API Key" %>
44
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :ollama_api_key, label: "Ollama API Key", hint: "Usually not required for local Ollama" %>
45
+ </div>
46
+ </div>
47
+
48
+ <!-- AWS Bedrock -->
49
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
50
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">AWS Bedrock</h3>
51
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
52
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :bedrock_api_key, label: "Access Key ID" %>
53
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :bedrock_secret_key, label: "Secret Access Key" %>
54
+ <%= render "api_key_field", f: f, config: config, resolved: resolved, attr: :bedrock_session_token, label: "Session Token", hint: "Optional, for temporary credentials" %>
55
+
56
+ <div>
57
+ <%= f.label :bedrock_region, "Region", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
58
+ <%= f.text_field :bedrock_region,
59
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
60
+ placeholder: "us-east-1" %>
61
+ </div>
62
+ </div>
63
+ </div>
64
+
65
+ <!-- Google Vertex AI -->
66
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
67
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Google Vertex AI</h3>
68
+ <div class="space-y-4">
69
+ <div>
70
+ <%= f.label :vertexai_credentials, "Service Account Credentials (JSON)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
71
+ <%= f.text_area :vertexai_credentials,
72
+ rows: 4,
73
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm font-mono",
74
+ placeholder: config.has_value?(:vertexai_credentials) ? "[Credentials set - leave blank to keep]" : '{"type": "service_account", ...}' %>
75
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Paste the full JSON service account key</p>
76
+ </div>
77
+
78
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
79
+ <div>
80
+ <%= f.label :vertexai_project_id, "Project ID", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
81
+ <%= f.text_field :vertexai_project_id,
82
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
83
+ placeholder: "my-gcp-project" %>
84
+ </div>
85
+
86
+ <div>
87
+ <%= f.label :vertexai_location, "Location", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
88
+ <%= f.text_field :vertexai_location,
89
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
90
+ placeholder: "us-central1" %>
91
+ </div>
92
+ </div>
93
+ </div>
94
+ </div>
95
+
96
+ <!-- Custom Endpoints -->
97
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
98
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Custom Endpoints</h3>
99
+ <p class="text-sm text-gray-500 dark:text-gray-400 mb-4">Override default API base URLs for self-hosted or proxy endpoints</p>
100
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
101
+ <div>
102
+ <%= f.label :openai_api_base, "OpenAI Base URL", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
103
+ <%= f.text_field :openai_api_base,
104
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
105
+ placeholder: "https://api.openai.com/v1" %>
106
+ </div>
107
+
108
+ <div>
109
+ <%= f.label :gemini_api_base, "Gemini Base URL", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
110
+ <%= f.text_field :gemini_api_base,
111
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
112
+ placeholder: "https://generativelanguage.googleapis.com" %>
113
+ </div>
114
+
115
+ <div>
116
+ <%= f.label :ollama_api_base, "Ollama Base URL", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
117
+ <%= f.text_field :ollama_api_base,
118
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
119
+ placeholder: "http://localhost:11434" %>
120
+ </div>
121
+
122
+ <div>
123
+ <%= f.label :gpustack_api_base, "GPUStack Base URL", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
124
+ <%= f.text_field :gpustack_api_base,
125
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
126
+ placeholder: "http://localhost:8000" %>
127
+ </div>
128
+
129
+ <div>
130
+ <%= f.label :xai_api_base, "xAI Base URL", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
131
+ <%= f.text_field :xai_api_base,
132
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
133
+ placeholder: "https://api.x.ai/v1" %>
134
+ </div>
135
+ </div>
136
+ </div>
137
+
138
+ <!-- OpenAI Options -->
139
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
140
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">OpenAI Options</h3>
141
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
142
+ <div>
143
+ <%= f.label :openai_organization_id, "Organization ID", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
144
+ <%= f.text_field :openai_organization_id,
145
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
146
+ placeholder: "org-..." %>
147
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">For users belonging to multiple organizations</p>
148
+ </div>
149
+
150
+ <div>
151
+ <%= f.label :openai_project_id, "Project ID", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
152
+ <%= f.text_field :openai_project_id,
153
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
154
+ placeholder: "proj_..." %>
155
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Scopes requests to a specific project</p>
156
+ </div>
157
+ </div>
158
+ </div>
159
+
160
+ <!-- Default Models -->
161
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
162
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Default Models</h3>
163
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
164
+ <div>
165
+ <%= f.label :default_model, "Default Chat Model", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
166
+ <%= f.text_field :default_model,
167
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
168
+ placeholder: "gpt-4o" %>
169
+ </div>
170
+
171
+ <div>
172
+ <%= f.label :default_embedding_model, "Default Embedding Model", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
173
+ <%= f.text_field :default_embedding_model,
174
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
175
+ placeholder: "text-embedding-3-small" %>
176
+ </div>
177
+
178
+ <div>
179
+ <%= f.label :default_image_model, "Default Image Model", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
180
+ <%= f.text_field :default_image_model,
181
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
182
+ placeholder: "dall-e-3" %>
183
+ </div>
184
+
185
+ <div>
186
+ <%= f.label :default_moderation_model, "Default Moderation Model", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
187
+ <%= f.text_field :default_moderation_model,
188
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
189
+ placeholder: "text-moderation-stable" %>
190
+ </div>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Connection Settings -->
195
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
196
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Connection Settings</h3>
197
+ <div class="grid grid-cols-1 md:grid-cols-3 gap-4">
198
+ <div>
199
+ <%= f.label :request_timeout, "Request Timeout (seconds)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
200
+ <%= f.number_field :request_timeout,
201
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
202
+ min: 1,
203
+ placeholder: "120" %>
204
+ </div>
205
+
206
+ <div>
207
+ <%= f.label :max_retries, "Max Retries", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
208
+ <%= f.number_field :max_retries,
209
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
210
+ min: 0,
211
+ max: 10,
212
+ placeholder: "3" %>
213
+ </div>
214
+
215
+ <div>
216
+ <%= f.label :retry_interval, "Retry Interval (seconds)", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
217
+ <%= f.number_field :retry_interval,
218
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
219
+ min: 0,
220
+ step: 0.1,
221
+ placeholder: "1.0" %>
222
+ </div>
223
+
224
+ <div>
225
+ <%= f.label :retry_backoff_factor, "Retry Backoff Factor", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
226
+ <%= f.number_field :retry_backoff_factor,
227
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
228
+ min: 1,
229
+ step: 0.1,
230
+ placeholder: "2.0" %>
231
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">Multiplier for exponential backoff</p>
232
+ </div>
233
+
234
+ <div>
235
+ <%= f.label :retry_interval_randomness, "Retry Randomness", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
236
+ <%= f.number_field :retry_interval_randomness,
237
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
238
+ min: 0,
239
+ max: 1,
240
+ step: 0.1,
241
+ placeholder: "0.5" %>
242
+ <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">0-1, adds jitter to retry interval</p>
243
+ </div>
244
+
245
+ <div>
246
+ <%= f.label :http_proxy, "HTTP Proxy", class: "block text-sm font-medium text-gray-700 dark:text-gray-300" %>
247
+ <%= f.text_field :http_proxy,
248
+ class: "mt-1 block w-full px-3 py-2 rounded-md border-gray-300 dark:border-gray-600 dark:bg-gray-700 dark:text-gray-100 shadow-sm focus:border-indigo-500 focus:ring-indigo-500 sm:text-sm",
249
+ placeholder: "http://proxy.example.com:8080" %>
250
+ </div>
251
+ </div>
252
+ </div>
253
+
254
+ <% if config.scope_type == "tenant" %>
255
+ <!-- Inheritance Settings (tenant only) -->
256
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6">
257
+ <h3 class="text-xs text-gray-500 dark:text-gray-400 uppercase tracking-wider mb-4">Inheritance</h3>
258
+ <div class="flex items-start">
259
+ <div class="flex items-center h-5">
260
+ <%= f.check_box :inherit_global_defaults, class: "h-4 w-4 text-indigo-600 focus:ring-indigo-500 border-gray-300 dark:border-gray-600 rounded" %>
261
+ </div>
262
+ <div class="ml-3">
263
+ <%= f.label :inherit_global_defaults, "Inherit Global Defaults", class: "text-sm font-medium text-gray-700 dark:text-gray-300" %>
264
+ <p class="text-sm text-gray-500 dark:text-gray-400">
265
+ When enabled, any unset values will fall back to the global configuration.
266
+ When disabled, only values explicitly set for this tenant will be used.
267
+ </p>
268
+ </div>
269
+ </div>
270
+ </div>
271
+ <% end %>
272
+
273
+ <!-- Form Actions -->
274
+ <div class="flex items-center justify-between bg-gray-50 dark:bg-gray-800/50 rounded-lg px-6 py-4">
275
+ <p class="text-sm text-gray-500 dark:text-gray-400">
276
+ <svg class="inline w-4 h-4 mr-1" fill="currentColor" viewBox="0 0 20 20">
277
+ <path fill-rule="evenodd" d="M5 9V7a5 5 0 0110 0v2a2 2 0 012 2v5a2 2 0 01-2 2H5a2 2 0 01-2-2v-5a2 2 0 012-2zm8-2v2H7V7a3 3 0 016 0z" clip-rule="evenodd"/>
278
+ </svg>
279
+ API keys are encrypted at rest
280
+ </p>
281
+
282
+ <div class="flex items-center space-x-3">
283
+ <% cancel_path = config.scope_type == "tenant" ? tenant_api_configuration_path(config.scope_id) : api_configuration_path %>
284
+ <%= link_to "Cancel", cancel_path, class: "px-4 py-2 text-sm font-medium text-gray-700 dark:text-gray-300 hover:text-gray-500" %>
285
+ <%= f.submit "Save Configuration", class: "inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500 cursor-pointer" %>
286
+ </div>
287
+ </div>
288
+ </div>
@@ -0,0 +1,95 @@
1
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
3
+ { label: "API Configuration", path: api_configuration_path },
4
+ { label: "Edit" }
5
+ ] %>
6
+
7
+ <!-- Header -->
8
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
9
+ <div class="flex items-center justify-between">
10
+ <div class="flex items-center space-x-3">
11
+ <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
12
+ <svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
13
+ <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" />
14
+ </svg>
15
+ </div>
16
+ <div>
17
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Edit Global API Configuration</h1>
18
+ <p class="text-sm text-gray-500 dark:text-gray-400">
19
+ Configure API keys and settings for all tenants
20
+ </p>
21
+ </div>
22
+ </div>
23
+ <button type="button" id="toggle-api-keys-btn"
24
+ class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
25
+ <svg id="eye-icon" class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
26
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
27
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
28
+ </svg>
29
+ <svg id="eye-off-icon" class="w-4 h-4 mr-1.5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
30
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
31
+ </svg>
32
+ <span id="toggle-btn-text">Show API Keys</span>
33
+ </button>
34
+ </div>
35
+ </div>
36
+
37
+ <!-- Info note about config override -->
38
+ <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
39
+ <div class="flex">
40
+ <svg class="w-5 h-5 text-blue-400 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
41
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
42
+ </svg>
43
+ <p class="text-sm text-blue-700 dark:text-blue-300">
44
+ Values entered here will override any configuration set via code or environment variables.
45
+ </p>
46
+ </div>
47
+ </div>
48
+
49
+ <%= form_with model: @config, url: api_configuration_path, method: :patch, local: true, scope: :api_configuration do |f| %>
50
+ <%= render "form", f: f, config: @config, resolved: @resolved %>
51
+ <% end %>
52
+
53
+ <script>
54
+ document.addEventListener('DOMContentLoaded', function() {
55
+ const toggleBtn = document.getElementById('toggle-api-keys-btn');
56
+ const eyeIcon = document.getElementById('eye-icon');
57
+ const eyeOffIcon = document.getElementById('eye-off-icon');
58
+ const btnText = document.getElementById('toggle-btn-text');
59
+ let visible = false;
60
+
61
+ toggleBtn.addEventListener('click', function() {
62
+ visible = !visible;
63
+
64
+ // Toggle input field types AND populate with DB values
65
+ document.querySelectorAll('[data-key-field]').forEach(function(input) {
66
+ input.type = visible ? 'text' : 'password';
67
+
68
+ // When showing for the first time, populate with DB value if available
69
+ if (visible && input.dataset.dbValue && !input.value) {
70
+ input.value = input.dataset.dbValue;
71
+ }
72
+ // Note: When hiding, we keep the value - it just shows as dots
73
+ });
74
+
75
+ // Toggle config value hints (show full vs masked) - unchanged
76
+ document.querySelectorAll('.config-value-hint').forEach(function(hint) {
77
+ const display = hint.querySelector('.config-value-display');
78
+ if (display) {
79
+ display.textContent = visible ? hint.dataset.fullValue : hint.dataset.maskedValue;
80
+ }
81
+ });
82
+
83
+ // Update button icon and text
84
+ if (visible) {
85
+ eyeIcon.classList.add('hidden');
86
+ eyeOffIcon.classList.remove('hidden');
87
+ btnText.textContent = 'Hide API Keys';
88
+ } else {
89
+ eyeIcon.classList.remove('hidden');
90
+ eyeOffIcon.classList.add('hidden');
91
+ btnText.textContent = 'Show API Keys';
92
+ }
93
+ });
94
+ });
95
+ </script>
@@ -0,0 +1,97 @@
1
+ <%= render "ruby_llm/agents/shared/breadcrumbs", items: [
2
+ { label: "Dashboard", path: ruby_llm_agents.root_path },
3
+ { label: "Tenants", path: tenants_path },
4
+ { label: @tenant_id },
5
+ { label: "API Configuration", path: tenant_api_configuration_path(@tenant_id) },
6
+ { label: "Edit" }
7
+ ] %>
8
+
9
+ <!-- Header -->
10
+ <div class="bg-white dark:bg-gray-800 rounded-lg shadow p-6 mb-6">
11
+ <div class="flex items-center justify-between">
12
+ <div class="flex items-center space-x-3">
13
+ <div class="flex-shrink-0 h-10 w-10 rounded-full bg-indigo-100 dark:bg-indigo-900/50 flex items-center justify-center">
14
+ <svg class="w-5 h-5 text-indigo-600 dark:text-indigo-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
15
+ <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" />
16
+ </svg>
17
+ </div>
18
+ <div>
19
+ <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Edit Tenant API Configuration</h1>
20
+ <p class="text-sm text-gray-500 dark:text-gray-400">
21
+ Tenant: <span class="font-medium"><%= @tenant_id %></span>
22
+ </p>
23
+ </div>
24
+ </div>
25
+ <button type="button" id="toggle-api-keys-btn"
26
+ class="inline-flex items-center px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 hover:bg-gray-200 dark:hover:bg-gray-600 rounded-md transition-colors">
27
+ <svg id="eye-icon" class="w-4 h-4 mr-1.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
29
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z" />
30
+ </svg>
31
+ <svg id="eye-off-icon" class="w-4 h-4 mr-1.5 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
32
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21" />
33
+ </svg>
34
+ <span id="toggle-btn-text">Show API Keys</span>
35
+ </button>
36
+ </div>
37
+ </div>
38
+
39
+ <!-- Info note about config override -->
40
+ <div class="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4 mb-6">
41
+ <div class="flex">
42
+ <svg class="w-5 h-5 text-blue-400 mr-3 flex-shrink-0" fill="currentColor" viewBox="0 0 20 20">
43
+ <path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z" clip-rule="evenodd"/>
44
+ </svg>
45
+ <p class="text-sm text-blue-700 dark:text-blue-300">
46
+ Values entered here will override any configuration set via code or environment variables.
47
+ </p>
48
+ </div>
49
+ </div>
50
+
51
+ <%= form_with model: @config, url: tenant_api_configuration_path(@tenant_id), method: :patch, local: true, scope: :api_configuration do |f| %>
52
+ <%= render "form", f: f, config: @config, resolved: @resolved %>
53
+ <% end %>
54
+
55
+ <script>
56
+ document.addEventListener('DOMContentLoaded', function() {
57
+ const toggleBtn = document.getElementById('toggle-api-keys-btn');
58
+ const eyeIcon = document.getElementById('eye-icon');
59
+ const eyeOffIcon = document.getElementById('eye-off-icon');
60
+ const btnText = document.getElementById('toggle-btn-text');
61
+ let visible = false;
62
+
63
+ toggleBtn.addEventListener('click', function() {
64
+ visible = !visible;
65
+
66
+ // Toggle input field types AND populate with DB values
67
+ document.querySelectorAll('[data-key-field]').forEach(function(input) {
68
+ input.type = visible ? 'text' : 'password';
69
+
70
+ // When showing for the first time, populate with DB value if available
71
+ if (visible && input.dataset.dbValue && !input.value) {
72
+ input.value = input.dataset.dbValue;
73
+ }
74
+ // Note: When hiding, we keep the value - it just shows as dots
75
+ });
76
+
77
+ // Toggle config value hints (show full vs masked) - unchanged
78
+ document.querySelectorAll('.config-value-hint').forEach(function(hint) {
79
+ const display = hint.querySelector('.config-value-display');
80
+ if (display) {
81
+ display.textContent = visible ? hint.dataset.fullValue : hint.dataset.maskedValue;
82
+ }
83
+ });
84
+
85
+ // Update button icon and text
86
+ if (visible) {
87
+ eyeIcon.classList.add('hidden');
88
+ eyeOffIcon.classList.remove('hidden');
89
+ btnText.textContent = 'Hide API Keys';
90
+ } else {
91
+ eyeIcon.classList.remove('hidden');
92
+ eyeOffIcon.classList.add('hidden');
93
+ btnText.textContent = 'Show API Keys';
94
+ }
95
+ });
96
+ });
97
+ </script>