ruby_llm-agents 1.3.3 → 2.0.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 (192) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +101 -334
  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 +46 -10
  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 +87 -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 +528 -989
  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 +9 -14
  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 +9 -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 +58 -262
  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 +52 -6
  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 +58 -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/attempt_tracker.rb +1 -0
  99. data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
  100. data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
  101. data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
  102. data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
  103. data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
  104. data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
  105. data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
  106. data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
  107. data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
  108. data/lib/ruby_llm/agents/rails/engine.rb +6 -6
  109. data/lib/ruby_llm/agents/results/base.rb +1 -49
  110. data/lib/ruby_llm/agents/text/embedder.rb +0 -1
  111. data/lib/ruby_llm/agents.rb +1 -9
  112. data/lib/tasks/ruby_llm_agents.rake +34 -0
  113. metadata +12 -81
  114. data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
  115. data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
  116. data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
  117. data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
  118. data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
  119. data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
  120. data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
  121. data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
  122. data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
  123. data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
  124. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
  125. data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
  126. data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
  127. data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
  128. data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
  129. data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
  130. data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
  131. data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
  132. data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
  133. data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
  134. data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
  135. data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
  136. data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
  137. data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
  138. data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
  139. data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
  140. data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
  141. data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
  142. data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
  143. data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
  144. data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
  145. data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
  146. data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
  147. data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
  148. data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
  149. data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
  150. data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
  151. data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
  152. data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
  153. data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
  154. data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
  155. data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
  156. data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
  157. data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
  158. data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
  159. data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
  160. data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
  161. data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
  162. data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
  163. data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
  164. data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
  165. data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
  166. data/lib/ruby_llm/agents/text/moderator.rb +0 -237
  167. data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
  168. data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
  169. data/lib/ruby_llm/agents/workflow/async.rb +0 -220
  170. data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
  171. data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
  172. data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
  173. data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
  174. data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
  175. data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
  176. data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
  177. data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
  178. data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
  179. data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
  180. data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
  181. data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
  182. data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
  183. data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
  184. data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
  185. data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
  186. data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
  187. data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
  188. data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
  189. data/lib/ruby_llm/agents/workflow/result.rb +0 -592
  190. data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
  191. data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
  192. data/lib/ruby_llm/agents/workflow/wait_result.rb +0 -213
@@ -1,456 +1,251 @@
1
- <!-- Page Header -->
2
- <div class="mb-6">
3
- <div class="flex items-center gap-2">
4
- <h1 class="text-2xl font-bold text-gray-900 dark:text-gray-100">Dashboard</h1>
5
- <%= render "ruby_llm/agents/shared/doc_link" %>
6
- </div>
7
- <p class="text-gray-500 dark:text-gray-400 mt-1">Overview of agent executions and performance metrics</p>
8
- </div>
9
-
10
1
  <!-- Action Center (only when critical alerts exist) -->
11
2
  <%= render partial: "ruby_llm/agents/dashboard/action_center", locals: { critical_alerts: @critical_alerts } %>
12
3
 
13
- <!-- Unified Metrics + Chart Component (Plausible-style) -->
14
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
15
- <!-- Header with range picker -->
16
- <div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
17
- <div class="flex items-center justify-between">
18
- <h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100">Activity</h3>
19
- <div class="relative range-dropdown-container">
20
- <button type="button"
21
- data-range-dropdown
22
- class="inline-flex items-center gap-2 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-300 bg-gray-100 dark:bg-gray-700 rounded-lg hover:bg-gray-200 dark:hover:bg-gray-600 transition-colors">
23
- <span><%= range_display_name(@selected_range) %></span>
24
- <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
25
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
26
- </svg>
27
- </button>
28
- <div class="range-dropdown-menu hidden absolute right-0 mt-2 w-56 bg-white dark:bg-gray-800 rounded-lg shadow-lg border border-gray-200 dark:border-gray-700 z-10">
29
- <div class="py-1">
30
- <%= link_to "Today", ruby_llm_agents.root_path(range: "today"), class: "block px-4 py-2 text-sm #{@selected_range == 'today' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
31
- <%= link_to "Last 7 Days", ruby_llm_agents.root_path(range: "7d"), class: "block px-4 py-2 text-sm #{@selected_range == '7d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
32
- <%= link_to "Last 30 Days", ruby_llm_agents.root_path(range: "30d"), class: "block px-4 py-2 text-sm #{@selected_range == '30d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
33
- <%= link_to "Last 60 Days", ruby_llm_agents.root_path(range: "60d"), class: "block px-4 py-2 text-sm #{@selected_range == '60d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
34
- <%= link_to "Last 90 Days", ruby_llm_agents.root_path(range: "90d"), class: "block px-4 py-2 text-sm #{@selected_range == '90d' ? 'bg-indigo-50 dark:bg-indigo-900/30 text-indigo-700 dark:text-indigo-300' : 'text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700'}" %>
35
- <hr class="border-gray-200 dark:border-gray-700 my-1">
36
- <button type="button" data-custom-range-toggle class="w-full text-left px-4 py-2 text-sm text-gray-700 dark:text-gray-300 hover:bg-gray-100 dark:hover:bg-gray-700">
37
- Custom Range...
38
- </button>
39
- </div>
40
- <div data-custom-range-form class="hidden border-t border-gray-200 dark:border-gray-700 p-3">
41
- <div class="space-y-3">
42
- <div>
43
- <label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">From</label>
44
- <input type="date"
45
- name="range_from"
46
- class="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
47
- </div>
48
- <div>
49
- <label class="block text-xs font-medium text-gray-500 dark:text-gray-400 mb-1">To</label>
50
- <input type="date"
51
- name="range_to"
52
- class="w-full px-2 py-1.5 text-sm border border-gray-300 dark:border-gray-600 rounded-md bg-white dark:bg-gray-700 text-gray-900 dark:text-gray-100 focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500">
53
- </div>
54
- <button type="button"
55
- data-apply-custom-range
56
- class="w-full px-3 py-1.5 text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 rounded-md transition-colors">
57
- Apply
58
- </button>
59
- </div>
60
- </div>
61
- </div>
62
- </div>
4
+ <!-- Stats Strip + Range Selector -->
5
+ <div class="flex items-center justify-between mb-3">
6
+ <div class="flex items-center gap-4">
7
+ <h1 class="text-[10px] font-medium text-gray-400 dark:text-gray-500 uppercase tracking-widest font-mono">overview</h1>
8
+ <div class="flex items-center gap-1.5 font-mono text-xs text-gray-400 dark:text-gray-500">
9
+ <% total = @now_strip[:success_today] + @now_strip[:errors_today] %>
10
+ <span class="text-gray-800 dark:text-gray-200"><%= number_with_delimiter(total) %></span> runs
11
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
12
+ <span class="<%= @now_strip[:errors_today] > 0 ? 'text-red-500' : 'text-gray-800 dark:text-gray-200' %>"><%= @now_strip[:errors_today] %></span> errors<% if total > 0 && @now_strip[:errors_today] > 0 %> <span class="text-gray-300 dark:text-gray-600">(<%= (@now_strip[:errors_today].to_f / total * 100).round(1) %>%)</span><% end %>
13
+ <span class="text-gray-300 dark:text-gray-700">&middot;</span>
14
+ <span class="text-gray-800 dark:text-gray-200">$<%= number_with_precision(@now_strip[:cost_today], precision: 2) %></span>
63
15
  </div>
64
16
  </div>
65
-
66
- <!-- Metric Row (Plausible-style: simple text, no cards) -->
67
- <div class="px-6 py-4 border-b border-gray-100 dark:border-gray-700">
68
- <div class="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-5 gap-4">
69
- <% metrics = [
70
- { key: "success", label: "SUCCESS", value: @now_strip[:success_today], change: @now_strip.dig(:comparisons, :success_change) },
71
- { key: "errors", label: "ERRORS", value: @now_strip[:errors_today], change: @now_strip.dig(:comparisons, :errors_change), is_error: @now_strip[:errors_today] > 0 },
72
- { key: "cost", label: "COST", value: "$#{number_with_precision(@now_strip[:cost_today], precision: 2)}", change: @now_strip.dig(:comparisons, :cost_change) },
73
- { key: "duration", label: "AVG TIME", value: format_duration_ms(@now_strip[:avg_duration_ms]), change: @now_strip.dig(:comparisons, :duration_change) },
74
- { key: "tokens", label: "TOKENS", value: number_to_human_short(@now_strip[:total_tokens]), change: @now_strip.dig(:comparisons, :tokens_change) }
75
- ] %>
76
-
77
- <% metrics.each_with_index do |metric, i| %>
78
- <button type="button"
79
- data-metric="<%= metric[:key] %>"
80
- data-index="<%= i %>"
81
- class="metric-btn group text-left transition-all pb-2 <%= i == 0 ? 'border-b-2 border-indigo-500' : 'border-b-2 border-transparent hover:border-gray-300 dark:hover:border-gray-600' %>">
82
- <span class="text-xs font-medium tracking-wide text-gray-500 dark:text-gray-400 uppercase"><%= metric[:label] %></span>
83
- <div class="flex items-baseline gap-1">
84
- <span class="text-2xl font-bold <%= metric[:is_error] ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-gray-100' %>"><%= metric[:value] %></span>
85
- <% if metric[:change] %>
86
- <span class="text-xs font-medium <%= metric[:change] > 0 ? (metric[:key].in?(%w[success tokens]) ? 'text-green-600' : 'text-red-600') : (metric[:key].in?(%w[success tokens]) ? 'text-red-600' : 'text-green-600') %>">
87
- <%= metric[:change] > 0 ? '+' : '' %><%= metric[:change] %>%
88
- </span>
89
- <% end %>
90
- </div>
91
- </button>
92
- <% end %>
93
- </div>
94
- </div>
95
-
96
- <!-- Chart -->
97
- <div id="activity-chart-container" class="px-4 pt-4 pb-6">
98
- <div id="activity-chart" style="width: 100%; height: 280px;"></div>
17
+ <div class="flex font-mono text-xs gap-0.5">
18
+ <%= link_to "today", ruby_llm_agents.root_path(range: "today"),
19
+ class: "px-2 py-0.5 #{@selected_range == 'today' ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}" %>
20
+ <%= link_to "7d", ruby_llm_agents.root_path(range: "7d"),
21
+ class: "px-2 py-0.5 #{@selected_range == '7d' ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}" %>
22
+ <%= link_to "30d", ruby_llm_agents.root_path(range: "30d"),
23
+ class: "px-2 py-0.5 #{@selected_range == '30d' ? 'text-gray-900 dark:text-gray-100' : 'text-gray-400 dark:text-gray-500 hover:text-gray-700 dark:hover:text-gray-300'}" %>
99
24
  </div>
25
+ </div>
100
26
 
101
- <script>
102
- (function() {
103
- const range = '<%= @selected_range %>';
104
- const chartUrl = '<%= ruby_llm_agents.chart_data_path %>?range=' + range;
105
- const hourMs = 3600000;
106
- const dayMs = 24 * hourMs;
107
-
108
- let chartData = null;
109
- let chart = null;
110
- let selectedMetric = 0;
111
-
112
- const metricConfig = [
113
- { name: 'Success', color: '#6366F1' },
114
- { name: 'Errors', color: '#6366F1' },
115
- { name: 'Cost', color: '#6366F1', isCurrency: true },
116
- { name: 'Duration', color: '#6366F1', isDuration: true },
117
- { name: 'Tokens', color: '#6366F1' }
118
- ];
119
-
120
- // Calculate time range in milliseconds based on range string
121
- function getTimeRangeMs(range) {
122
- if (range === 'today') return dayMs;
123
- if (range === '7d') return 7 * dayMs;
124
- if (range === '30d') return 30 * dayMs;
125
- if (range === '60d') return 60 * dayMs;
126
- if (range === '90d') return 90 * dayMs;
127
- // Custom range: calculate from dates
128
- if (range.includes('_')) {
129
- const [from, to] = range.split('_');
130
- const fromDate = new Date(from);
131
- const toDate = new Date(to);
132
- return toDate - fromDate + dayMs;
133
- }
134
- return dayMs;
135
- }
136
-
137
- function convertToDatetimePoints(data, range) {
138
- const now = Date.now();
139
- const numPoints = data.length;
140
- if (range === 'today') {
141
- return data.map((val, i) => [now - (numPoints - 1 - i) * hourMs, val]);
142
- } else {
143
- return data.map((val, i) => [now - (numPoints - 1 - i) * dayMs, val]);
144
- }
145
- }
146
-
147
- function formatNumber(num) {
148
- return num.toLocaleString();
149
- }
150
-
151
- function formatDuration(ms) {
152
- if (!ms || ms === 0) return '0ms';
153
- if (ms < 1000) return ms.toFixed(0) + 'ms';
154
- if (ms < 60000) return (ms / 1000).toFixed(1) + 's';
155
- return (ms / 60000).toFixed(1) + 'm';
156
- }
157
-
158
- function updateChart(metricIndex) {
159
- if (!chartData || !chart) return;
160
-
161
- const config = metricConfig[metricIndex];
162
- const series = chartData.series[metricIndex];
163
-
164
- chart.series[0].update({
165
- name: config.name,
166
- data: convertToDatetimePoints(series.data, range),
167
- color: config.color
168
- }, false);
169
-
170
- chart.yAxis[0].update({
171
- labels: {
172
- style: { color: '#9CA3AF' },
173
- formatter: function() {
174
- if (config.isCurrency) return '$' + this.value;
175
- if (config.isDuration) return formatDuration(this.value);
176
- return this.value;
177
- }
178
- }
179
- }, false);
180
-
181
- chart.tooltip.update({
182
- formatter: function() {
183
- const dateFormat = range === 'today' ? '%H:%M' : '%b %d';
184
- const dateStr = Highcharts.dateFormat(dateFormat, this.x);
185
- let valueStr;
186
- if (config.isCurrency) valueStr = '$' + this.y.toFixed(4);
187
- else if (config.isDuration) valueStr = formatDuration(this.y);
188
- else valueStr = formatNumber(this.y);
189
- return '<b>' + dateStr + '</b><br/>' + config.name + ': ' + valueStr;
190
- }
191
- });
192
-
193
- chart.redraw();
194
-
195
- // Update selection styles
196
- document.querySelectorAll('.metric-btn').forEach((btn, i) => {
197
- btn.classList.remove('border-indigo-500', 'border-transparent');
198
- btn.classList.add(i === metricIndex ? 'border-indigo-500' : 'border-transparent');
199
- });
200
- }
201
-
202
- function initChart() {
203
- fetch(chartUrl)
204
- .then(res => res.json())
205
- .then(data => {
206
- chartData = data;
207
- const config = metricConfig[0];
208
- const timeRange = getTimeRangeMs(range);
209
- const dateFormat = range === 'today' ? '%H:%M' : '%b %d';
210
- const now = Date.now();
211
-
212
- chart = Highcharts.chart('activity-chart', {
213
- chart: { type: 'areaspline', backgroundColor: 'transparent', spacing: [10, 0, 10, 0] },
27
+ <!-- Activity Chart -->
28
+ <div id="activity-chart" style="width: 100%; height: 180px;"></div>
29
+
30
+ <script>
31
+ (function() {
32
+ const range = '<%= @selected_range %>';
33
+ const chartUrl = '<%= ruby_llm_agents.chart_data_path %>?range=' + range;
34
+ const hourMs = 3600000;
35
+ const dayMs = 24 * hourMs;
36
+
37
+ function getTimeRangeMs(r) {
38
+ if (r === 'today') return dayMs;
39
+ if (r === '7d') return 7 * dayMs;
40
+ return 30 * dayMs;
41
+ }
42
+
43
+ function toDatetimePoints(data, r) {
44
+ const now = Date.now();
45
+ const step = r === 'today' ? hourMs : dayMs;
46
+ return data.map((val, i) => [now - (data.length - 1 - i) * step, val]);
47
+ }
48
+
49
+ function initChart() {
50
+ fetch(chartUrl)
51
+ .then(res => res.json())
52
+ .then(data => {
53
+ const now = Date.now();
54
+ const fmt = range === 'today' ? '%H:%M' : '%b %d';
55
+
56
+ Highcharts.chart('activity-chart', {
57
+ chart: { type: 'areaspline', backgroundColor: 'transparent', spacing: [5, 0, 0, 0] },
58
+ title: { text: null },
59
+ xAxis: {
60
+ type: 'datetime',
61
+ min: now - getTimeRangeMs(range),
62
+ max: now,
63
+ labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' }, format: '{value:' + fmt + '}' },
64
+ lineColor: 'transparent',
65
+ tickLength: 0,
66
+ gridLineWidth: 0
67
+ },
68
+ yAxis: {
214
69
  title: { text: null },
215
- xAxis: {
216
- type: 'datetime',
217
- min: now - timeRange,
218
- max: now,
219
- labels: { style: { color: '#9CA3AF', fontSize: '11px' }, format: '{value:' + dateFormat + '}' },
220
- lineColor: 'transparent',
221
- tickLength: 0
70
+ min: 0,
71
+ allowDecimals: false,
72
+ labels: { style: { color: '#6B7280', fontSize: '9px', fontFamily: 'ui-monospace, monospace' } },
73
+ gridLineColor: 'rgba(107, 114, 128, 0.08)'
74
+ },
75
+ legend: { enabled: false },
76
+ credits: { enabled: false },
77
+ tooltip: {
78
+ backgroundColor: 'rgba(0, 0, 0, 0.85)',
79
+ borderColor: 'transparent',
80
+ borderRadius: 3,
81
+ style: { color: '#E5E7EB', fontSize: '10px', fontFamily: 'ui-monospace, monospace' },
82
+ shared: true,
83
+ formatter: function() {
84
+ let html = '<span style="color:#9CA3AF">' + Highcharts.dateFormat(fmt, this.x) + '</span>';
85
+ this.points.forEach(p => html += '<br/>' + p.series.name + ': <b>' + p.y + '</b>');
86
+ return html;
87
+ }
88
+ },
89
+ plotOptions: {
90
+ areaspline: {
91
+ stacking: 'normal',
92
+ lineWidth: 1.5,
93
+ marker: { enabled: false, states: { hover: { enabled: true, radius: 2 } } }
94
+ }
95
+ },
96
+ series: [
97
+ {
98
+ name: 'errors',
99
+ data: toDatetimePoints(data.series[1].data, range),
100
+ color: '#EF4444',
101
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(239, 68, 68, 0.08)'], [1, 'rgba(239, 68, 68, 0)']] }
222
102
  },
223
- yAxis: {
224
- title: { text: null },
225
- min: 0,
226
- allowDecimals: false,
227
- labels: { style: { color: '#9CA3AF', fontSize: '11px' } },
228
- gridLineColor: 'rgba(156, 163, 175, 0.15)'
229
- },
230
- legend: { enabled: false },
231
- credits: { enabled: false },
232
- tooltip: {
233
- backgroundColor: 'rgba(17, 24, 39, 0.95)',
234
- borderColor: 'transparent',
235
- borderRadius: 8,
236
- style: { color: '#F3F4F6', fontSize: '12px' },
237
- formatter: function() {
238
- return '<b>' + Highcharts.dateFormat(dateFormat, this.x) + '</b><br/>' +
239
- config.name + ': ' + formatNumber(this.y);
240
- }
241
- },
242
- plotOptions: {
243
- areaspline: {
244
- fillColor: {
245
- linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 },
246
- stops: [[0, 'rgba(99, 102, 241, 0.3)'], [1, 'rgba(99, 102, 241, 0)']]
247
- },
248
- lineWidth: 2,
249
- marker: { enabled: false, states: { hover: { enabled: true, radius: 4 } } }
250
- }
251
- },
252
- series: [{
253
- name: config.name,
254
- data: convertToDatetimePoints(data.series[0].data, range),
255
- color: config.color
256
- }]
257
- });
258
- })
259
- .catch(err => console.log('[RubyLLM::Agents] Chart error:', err));
260
-
261
- document.querySelectorAll('.metric-btn').forEach(btn => {
262
- btn.addEventListener('click', function() {
263
- const index = parseInt(this.dataset.index);
264
- selectedMetric = index;
265
- updateChart(index);
103
+ {
104
+ name: 'success',
105
+ data: toDatetimePoints(data.series[0].data, range),
106
+ color: '#10B981',
107
+ fillColor: { linearGradient: { x1: 0, y1: 0, x2: 0, y2: 1 }, stops: [[0, 'rgba(16, 185, 129, 0.08)'], [1, 'rgba(16, 185, 129, 0)']] }
108
+ }
109
+ ]
266
110
  });
267
- });
268
- }
111
+ })
112
+ .catch(err => console.log('[RubyLLM::Agents] Chart error:', err));
113
+ }
269
114
 
270
- // Dropdown toggle functionality
271
- function initDropdown() {
272
- const dropdownBtn = document.querySelector('[data-range-dropdown]');
273
- const dropdownMenu = document.querySelector('.range-dropdown-menu');
274
- const customRangeToggle = document.querySelector('[data-custom-range-toggle]');
275
- const customRangeForm = document.querySelector('[data-custom-range-form]');
276
- const applyBtn = document.querySelector('[data-apply-custom-range]');
115
+ document.readyState === 'loading'
116
+ ? document.addEventListener('DOMContentLoaded', initChart)
117
+ : initChart();
118
+ })();
119
+ </script>
277
120
 
278
- if (dropdownBtn && dropdownMenu) {
279
- // Toggle dropdown on button click
280
- dropdownBtn.addEventListener('click', function(e) {
281
- e.stopPropagation();
282
- dropdownMenu.classList.toggle('hidden');
283
- });
284
-
285
- // Close dropdown when clicking outside
286
- document.addEventListener('click', function(e) {
287
- if (!e.target.closest('.range-dropdown-container')) {
288
- dropdownMenu.classList.add('hidden');
289
- if (customRangeForm) customRangeForm.classList.add('hidden');
290
- }
291
- });
292
- }
293
-
294
- // Toggle custom range form
295
- if (customRangeToggle && customRangeForm) {
296
- customRangeToggle.addEventListener('click', function(e) {
297
- e.stopPropagation();
298
- customRangeForm.classList.toggle('hidden');
299
- });
300
- }
301
-
302
- // Apply custom range
303
- if (applyBtn) {
304
- applyBtn.addEventListener('click', function() {
305
- const fromInput = document.querySelector('[name="range_from"]');
306
- const toInput = document.querySelector('[name="range_to"]');
307
- const from = fromInput ? fromInput.value : '';
308
- const to = toInput ? toInput.value : '';
309
-
310
- if (from && to) {
311
- window.location.href = '?range=' + from + '_' + to;
312
- }
313
- });
314
- }
315
- }
121
+ <!-- Tenant Budget (when viewing specific tenant) -->
122
+ <%= render partial: "ruby_llm/agents/dashboard/tenant_budget", locals: { tenant_budget: @tenant_budget } %>
316
123
 
317
- if (document.readyState === 'loading') {
318
- document.addEventListener('DOMContentLoaded', function() {
319
- initChart();
320
- initDropdown();
321
- });
322
- } else {
323
- initChart();
324
- initDropdown();
325
- }
326
- })();
327
- </script>
124
+ <!-- ── recent ──────────────────────────────── -->
125
+ <div class="flex items-center gap-3 mt-8 mb-3">
126
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">recent</span>
127
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
128
+ <%= link_to "all →", ruby_llm_agents.executions_path, class: "text-[10px] font-mono text-gray-400 dark:text-gray-600 hover:text-gray-600 dark:hover:text-gray-400" %>
328
129
  </div>
329
130
 
330
- <!-- Tenant Budget (when viewing specific tenant) -->
331
- <%= render partial: "ruby_llm/agents/dashboard/tenant_budget", locals: { tenant_budget: @tenant_budget } %>
131
+ <% if @recent_executions.any? %>
132
+ <div class="font-mono text-xs space-y-px">
133
+ <% @recent_executions.first(5).each do |execution| %>
134
+ <div class="group flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50 cursor-pointer"
135
+ onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'">
136
+ <span class="text-gray-300 dark:text-gray-700">▸</span>
137
+ <span class="w-28 truncate text-gray-900 dark:text-gray-200"><%= execution.agent_type.gsub(/Agent$/, '') %></span>
138
+ <%# Status dot %>
139
+ <% dot_color = if execution.status_running?
140
+ 'bg-blue-500 animate-pulse'
141
+ elsif execution.status_error?
142
+ 'bg-red-500'
143
+ elsif execution.status.to_s == 'timeout'
144
+ 'bg-yellow-500'
145
+ else
146
+ 'bg-green-500'
147
+ end %>
148
+ <span class="w-1.5 h-1.5 rounded-full flex-shrink-0 <%= dot_color %>"></span>
149
+ <%# Duration %>
150
+ <span class="w-14 text-right text-gray-500 dark:text-gray-400">
151
+ <% if execution.status_running? %>
152
+ <span class="text-blue-500 animate-pulse">...</span>
153
+ <% else %>
154
+ <%= execution.duration_ms ? format_duration_ms(execution.duration_ms) : '—' %>
155
+ <% end %>
156
+ </span>
157
+ <%# Cost %>
158
+ <span class="w-16 text-right text-gray-500 dark:text-gray-400 hidden sm:inline">$<%= number_with_precision(execution.total_cost.to_f, precision: 4) %></span>
159
+ <%# Model %>
160
+ <span class="flex-1 truncate text-gray-400 dark:text-gray-600 hidden md:inline"><%= execution.model_id.to_s.split('/').last %></span>
161
+ <%# Time ago %>
162
+ <span class="text-gray-400 dark:text-gray-600 text-right whitespace-nowrap"><%= time_ago_in_words(execution.created_at) %></span>
163
+ </div>
164
+ <% if execution.status_error? && execution.error_class.present? %>
165
+ <div class="flex items-center gap-1 pl-7 py-0.5 text-red-400 dark:text-red-500/70 text-xs font-mono">
166
+ <span class="text-gray-300 dark:text-gray-700">└</span>
167
+ <span class="truncate"><%= execution.error_class.to_s.split("::").last %><%= ": #{execution.error_message.truncate(60)}" if execution.error_message.present? %></span>
168
+ </div>
169
+ <% end %>
170
+ <% end %>
171
+ </div>
172
+ <% else %>
173
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-4 px-2">no activity yet</div>
174
+ <% end %>
175
+
176
+ <!-- ── agents + errors ──────────────────────── -->
177
+ <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mt-8">
178
+ <!-- Agents -->
179
+ <div>
180
+ <div class="flex items-center gap-3 mb-3">
181
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">agents</span>
182
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
183
+ </div>
184
+ <%
185
+ all_stats = [@agent_stats, @embedder_stats, @transcriber_stats, @speaker_stats, @image_generator_stats]
186
+ .flatten.compact.select { |a| a[:executions].to_i > 0 }.sort_by { |a| -a[:executions].to_i }.first(8)
187
+ %>
188
+ <% if all_stats.any? %>
189
+ <div class="font-mono text-xs space-y-px">
190
+ <% all_stats.each do |item| %>
191
+ <div class="flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50">
192
+ <span class="w-32 truncate text-gray-900 dark:text-gray-200"><%= item[:agent_type].to_s.demodulize %></span>
193
+ <span class="text-gray-500 dark:text-gray-400"><%= number_with_delimiter(item[:executions]) %><span class="text-gray-400 dark:text-gray-600">r</span></span>
194
+ <span class="text-gray-500 dark:text-gray-400 hidden sm:inline">$<%= number_with_precision(item[:total_cost], precision: 2) %></span>
195
+ <span class="ml-auto <%= item[:success_rate] >= 95 ? 'text-green-500' : item[:success_rate] >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= item[:success_rate].round %>%</span>
196
+ </div>
197
+ <% end %>
198
+ </div>
199
+ <% else %>
200
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-2 px-2">&mdash;</div>
201
+ <% end %>
202
+ </div>
332
203
 
333
- <!-- Recent Activity Table -->
334
- <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 mb-6">
335
- <div class="px-6 py-3 border-b border-gray-100 dark:border-gray-700">
336
- <div class="flex justify-between items-center">
337
- <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Recent Activity</h3>
338
- <%= link_to "View All", ruby_llm_agents.executions_path, class: "text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm font-medium" %>
204
+ <!-- Errors -->
205
+ <div>
206
+ <div class="flex items-center gap-3 mb-3">
207
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">errors</span>
208
+ <% if @top_errors.any? %><span class="text-[10px] font-mono text-red-400 dark:text-red-500/70">(<%= @top_errors.sum { |e| e[:count] } %>)</span><% end %>
209
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
339
210
  </div>
211
+ <% if @top_errors.any? %>
212
+ <div class="font-mono text-xs space-y-px">
213
+ <% @top_errors.first(5).each do |error| %>
214
+ <div class="flex items-center gap-3 py-1.5 px-2 -mx-2">
215
+ <span class="w-1.5 h-1.5 rounded-full bg-red-500 flex-shrink-0"></span>
216
+ <span class="flex-1 truncate text-gray-900 dark:text-gray-200"><%= error[:error_class].to_s.split("::").last %></span>
217
+ <span class="text-red-500 font-medium"><%= error[:count] %>&times;</span>
218
+ <span class="text-gray-400 dark:text-gray-600 whitespace-nowrap"><%= error[:last_seen] ? time_ago_in_words(error[:last_seen]) : '—' %></span>
219
+ </div>
220
+ <% end %>
221
+ </div>
222
+ <% else %>
223
+ <div class="font-mono text-xs text-green-500/80 py-2 px-2 flex items-center gap-2">
224
+ <span class="w-1.5 h-1.5 rounded-full bg-green-500"></span> none
225
+ </div>
226
+ <% end %>
340
227
  </div>
228
+ </div>
341
229
 
342
- <% if @recent_executions.any? %>
343
- <div class="overflow-x-auto">
344
- <table class="w-full text-sm">
345
- <thead>
346
- <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider border-b border-gray-100 dark:border-gray-700">
347
- <th class="px-4 py-3 w-8"></th>
348
- <th class="px-4 py-3">Agent</th>
349
- <th class="px-4 py-3 hidden sm:table-cell">Model</th>
350
- <th class="px-4 py-3 text-right">Tokens</th>
351
- <th class="px-4 py-3 text-right hidden sm:table-cell">Cost</th>
352
- <th class="px-4 py-3 text-right hidden md:table-cell">Time</th>
353
- <th class="px-4 py-3 text-right">When</th>
354
- </tr>
355
- </thead>
356
- <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
357
- <% @recent_executions.first(5).each do |execution| %>
358
- <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50 transition-colors cursor-pointer <%= execution.status_error? ? 'bg-red-50/50 dark:bg-red-900/10' : '' %>"
359
- onclick="window.location='<%= ruby_llm_agents.execution_path(execution) %>'"
360
- <% if execution.status_error? && execution.error_message.present? %>
361
- title="<%= execution.error_class %>: <%= execution.error_message.truncate(100) %>"
362
- <% end %>>
363
- <!-- Status -->
364
- <td class="px-4 py-3">
365
- <%= render "ruby_llm/agents/shared/status_dot", status: execution.status %>
366
- </td>
367
- <!-- Agent + Badges -->
368
- <td class="px-4 py-3">
369
- <div class="flex items-center gap-1.5 min-w-0">
370
- <% if execution.respond_to?(:workflow_type) && execution.workflow_type.present? %>
371
- <%= render "ruby_llm/agents/shared/workflow_type_badge", workflow_type: execution.workflow_type, size: :xs, show_label: false %>
372
- <% end %>
373
- <span class="font-medium text-gray-900 dark:text-gray-100 truncate">
374
- <%= execution.agent_type.gsub(/Agent$|Workflow$|Pipeline$|Parallel$|Router$/, '') %>
375
- </span>
376
- <% unless execution.status_running? %>
377
- <% if execution.streaming? %>
378
- <svg class="w-3.5 h-3.5 text-cyan-500 flex-shrink-0" title="Streaming" fill="none" stroke="currentColor" viewBox="0 0 24 24">
379
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 10V3L4 14h7v7l9-11h-7z"/>
380
- </svg>
381
- <% end %>
382
- <% if execution.cache_hit? %>
383
- <svg class="w-3.5 h-3.5 text-purple-500 flex-shrink-0" title="Cached" fill="none" stroke="currentColor" viewBox="0 0 24 24">
384
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4"/>
385
- </svg>
386
- <% end %>
387
- <% if execution.rate_limited? %>
388
- <svg class="w-3.5 h-3.5 text-orange-500 flex-shrink-0" title="Rate Limited" fill="none" stroke="currentColor" viewBox="0 0 24 24">
389
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"/>
390
- </svg>
391
- <% end %>
392
- <% end %>
393
- <% if execution.status_error? %>
394
- <svg class="w-3.5 h-3.5 text-red-500 flex-shrink-0" title="Error" fill="none" stroke="currentColor" viewBox="0 0 24 24">
395
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
396
- </svg>
397
- <% end %>
398
- </div>
399
- </td>
400
- <!-- Model -->
401
- <td class="px-4 py-3 hidden sm:table-cell">
402
- <span class="font-mono text-xs text-gray-500 dark:text-gray-400 truncate max-w-[120px] block">
403
- <%= execution.model_id || '-' %>
404
- </span>
405
- </td>
406
- <!-- Tokens -->
407
- <td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300">
408
- <% if execution.status_running? %>
409
- <span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
410
- <% else %>
411
- <%= execution.total_tokens ? number_to_human_short(execution.total_tokens) : '-' %>
412
- <% end %>
413
- </td>
414
- <!-- Cost -->
415
- <td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300 hidden sm:table-cell">
416
- <% if execution.status_running? %>
417
- <span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
418
- <% else %>
419
- <%= execution.total_cost ? "$#{number_with_precision(execution.total_cost, precision: 2)}" : '-' %>
420
- <% end %>
421
- </td>
422
- <!-- Duration -->
423
- <td class="px-4 py-3 text-right text-gray-600 dark:text-gray-300 hidden md:table-cell">
424
- <% if execution.status_running? %>
425
- <span class="text-blue-500 dark:text-blue-400 animate-pulse">...</span>
426
- <% else %>
427
- <%= execution.duration_ms ? format_duration_ms(execution.duration_ms) : '-' %>
428
- <% end %>
429
- </td>
430
- <!-- When -->
431
- <td class="px-4 py-3 text-right text-gray-500 dark:text-gray-400 whitespace-nowrap">
432
- <%= time_ago_in_words(execution.created_at) %> ago
433
- </td>
434
- </tr>
435
- <% end %>
436
- </tbody>
437
- </table>
230
+ <!-- ── models ──────────────────────────────── -->
231
+ <div class="mt-8">
232
+ <div class="flex items-center gap-3 mb-3">
233
+ <span class="text-[10px] font-medium text-gray-400 dark:text-gray-600 uppercase tracking-widest font-mono">models</span>
234
+ <div class="flex-1 border-t border-gray-200 dark:border-gray-800"></div>
235
+ </div>
236
+ <% if @model_stats.any? %>
237
+ <div class="font-mono text-xs space-y-px">
238
+ <% @model_stats.first(6).each do |model| %>
239
+ <div class="flex items-center gap-3 py-1.5 px-2 -mx-2 rounded hover:bg-gray-100 dark:hover:bg-gray-800/50">
240
+ <span class="w-40 truncate text-gray-900 dark:text-gray-200"><%= model[:model_id].to_s.split('/').last.truncate(28) %></span>
241
+ <span class="text-gray-500 dark:text-gray-400"><%= number_with_delimiter(model[:executions]) %><span class="text-gray-400 dark:text-gray-600">r</span></span>
242
+ <span class="text-gray-500 dark:text-gray-400 hidden sm:inline">$<%= number_with_precision(model[:total_cost], precision: 2) %></span>
243
+ <span class="text-gray-500 dark:text-gray-400 hidden md:inline"><%= format_duration_ms(model[:avg_duration_ms]) %></span>
244
+ <span class="ml-auto <%= model[:success_rate] >= 95 ? 'text-green-500' : model[:success_rate] >= 80 ? 'text-yellow-500' : 'text-red-500' %>"><%= model[:success_rate].round %>%</span>
245
+ </div>
246
+ <% end %>
438
247
  </div>
439
248
  <% else %>
440
- <div class="py-8 text-center text-gray-500 dark:text-gray-400">
441
- <p class="text-sm">No executions yet</p>
442
- </div>
249
+ <div class="font-mono text-xs text-gray-400 dark:text-gray-600 py-2 px-2">&mdash;</div>
443
250
  <% end %>
444
251
  </div>
445
-
446
- <!-- Agent Comparison + Top Errors -->
447
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
448
- <%= render partial: "ruby_llm/agents/dashboard/agent_comparison", locals: { agent_stats: @agent_stats } %>
449
- <%= render partial: "ruby_llm/agents/dashboard/top_errors", locals: { top_errors: @top_errors } %>
450
- </div>
451
-
452
- <!-- Model Performance + Cost Breakdown -->
453
- <div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
454
- <%= render partial: "ruby_llm/agents/dashboard/model_comparison", locals: { model_stats: @model_stats } %>
455
- <%= render partial: "ruby_llm/agents/dashboard/model_cost_breakdown", locals: { model_stats: @model_stats } %>
456
- </div>