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.
- checksums.yaml +4 -4
- data/README.md +101 -334
- data/app/controllers/concerns/ruby_llm/agents/sortable.rb +0 -1
- data/app/controllers/ruby_llm/agents/agents_controller.rb +5 -56
- data/app/controllers/ruby_llm/agents/dashboard_controller.rb +22 -106
- data/app/controllers/ruby_llm/agents/executions_controller.rb +4 -114
- data/app/controllers/ruby_llm/agents/tenants_controller.rb +30 -2
- data/app/helpers/ruby_llm/agents/application_helper.rb +19 -53
- data/app/models/ruby_llm/agents/execution/analytics.rb +13 -54
- data/app/models/ruby_llm/agents/execution/scopes.rb +61 -14
- data/app/models/ruby_llm/agents/execution.rb +46 -10
- data/app/models/ruby_llm/agents/execution_detail.rb +18 -0
- data/app/models/ruby_llm/agents/tenant/budgetable.rb +132 -24
- data/app/models/ruby_llm/agents/tenant/incrementable.rb +117 -0
- data/app/models/ruby_llm/agents/tenant/resettable.rb +128 -0
- data/app/models/ruby_llm/agents/tenant/trackable.rb +46 -12
- data/app/models/ruby_llm/agents/tenant.rb +2 -3
- data/app/models/ruby_llm/agents/tenant_budget.rb +6 -3
- data/app/services/ruby_llm/agents/agent_registry.rb +6 -112
- data/app/views/layouts/ruby_llm/agents/application.html.erb +87 -252
- data/app/views/ruby_llm/agents/agents/_config_agent.html.erb +71 -218
- data/app/views/ruby_llm/agents/agents/_config_embedder.html.erb +20 -63
- data/app/views/ruby_llm/agents/agents/_config_image_generator.html.erb +44 -131
- data/app/views/ruby_llm/agents/agents/_config_moderator.html.erb +16 -57
- data/app/views/ruby_llm/agents/agents/_config_speaker.html.erb +39 -104
- data/app/views/ruby_llm/agents/agents/_config_transcriber.html.erb +29 -82
- data/app/views/ruby_llm/agents/agents/_empty_state.html.erb +4 -14
- data/app/views/ruby_llm/agents/agents/index.html.erb +105 -274
- data/app/views/ruby_llm/agents/agents/show.html.erb +248 -378
- data/app/views/ruby_llm/agents/dashboard/_action_center.html.erb +29 -52
- data/app/views/ruby_llm/agents/dashboard/_tenant_budget.html.erb +73 -99
- data/app/views/ruby_llm/agents/dashboard/index.html.erb +228 -433
- data/app/views/ruby_llm/agents/executions/_execution.html.erb +1 -1
- data/app/views/ruby_llm/agents/executions/_filters.html.erb +4 -25
- data/app/views/ruby_llm/agents/executions/_list.html.erb +111 -152
- data/app/views/ruby_llm/agents/executions/index.html.erb +5 -7
- data/app/views/ruby_llm/agents/executions/show.html.erb +528 -989
- data/app/views/ruby_llm/agents/shared/_agent_type_badge.html.erb +5 -21
- data/app/views/ruby_llm/agents/shared/_executions_table.html.erb +70 -191
- data/app/views/ruby_llm/agents/shared/_filter_dropdown.html.erb +16 -44
- data/app/views/ruby_llm/agents/shared/_select_dropdown.html.erb +12 -41
- data/app/views/ruby_llm/agents/shared/_status_badge.html.erb +11 -65
- data/app/views/ruby_llm/agents/shared/_tenant_filter.html.erb +6 -5
- data/app/views/ruby_llm/agents/system_config/show.html.erb +240 -351
- data/app/views/ruby_llm/agents/tenants/_form.html.erb +67 -77
- data/app/views/ruby_llm/agents/tenants/edit.html.erb +7 -9
- data/app/views/ruby_llm/agents/tenants/index.html.erb +100 -122
- data/app/views/ruby_llm/agents/tenants/show.html.erb +146 -336
- data/config/routes.rb +0 -13
- data/lib/generators/ruby_llm_agents/install_generator.rb +9 -14
- data/lib/generators/ruby_llm_agents/migrate_structure_generator.rb +2 -12
- data/lib/generators/ruby_llm_agents/restructure_generator.rb +0 -2
- data/lib/generators/ruby_llm_agents/templates/add_usage_counters_to_tenants_migration.rb.tt +37 -0
- data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +1 -2
- data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +1 -1
- data/lib/generators/ruby_llm_agents/templates/application_image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/create_execution_details_migration.rb.tt +27 -0
- data/lib/generators/ruby_llm_agents/templates/create_tenants_migration.rb.tt +25 -0
- data/lib/generators/ruby_llm_agents/templates/image_pipeline.rb.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +9 -12
- data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +40 -71
- data/lib/generators/ruby_llm_agents/templates/remove_agent_version_migration.rb.tt +13 -0
- data/lib/generators/ruby_llm_agents/templates/remove_workflow_columns_migration.rb.tt +19 -0
- data/lib/generators/ruby_llm_agents/templates/skills/AGENTS.md.tt +2 -4
- data/lib/generators/ruby_llm_agents/templates/skills/IMAGE_PIPELINES.md.tt +0 -1
- data/lib/generators/ruby_llm_agents/templates/split_execution_details_migration.rb.tt +232 -0
- data/lib/generators/ruby_llm_agents/upgrade_generator.rb +58 -262
- data/lib/ruby_llm/agents/audio/speaker.rb +0 -1
- data/lib/ruby_llm/agents/audio/transcriber.rb +0 -1
- data/lib/ruby_llm/agents/base_agent.rb +52 -6
- data/lib/ruby_llm/agents/core/base/callbacks.rb +142 -0
- data/lib/ruby_llm/agents/core/base.rb +23 -55
- data/lib/ruby_llm/agents/core/configuration.rb +58 -117
- data/lib/ruby_llm/agents/core/errors.rb +0 -58
- data/lib/ruby_llm/agents/core/instrumentation.rb +157 -110
- data/lib/ruby_llm/agents/core/llm_tenant.rb +8 -7
- data/lib/ruby_llm/agents/core/version.rb +1 -1
- data/lib/ruby_llm/agents/dsl/base.rb +157 -17
- data/lib/ruby_llm/agents/dsl/caching.rb +33 -2
- data/lib/ruby_llm/agents/dsl/reliability.rb +148 -0
- data/lib/ruby_llm/agents/dsl.rb +1 -2
- data/lib/ruby_llm/agents/image/analyzer/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/background_remover/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/concerns/image_operation_dsl.rb +1 -13
- data/lib/ruby_llm/agents/image/concerns/image_operation_execution.rb +2 -2
- data/lib/ruby_llm/agents/image/editor/dsl.rb +0 -14
- data/lib/ruby_llm/agents/image/editor/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/editor.rb +0 -1
- data/lib/ruby_llm/agents/image/generator.rb +0 -21
- data/lib/ruby_llm/agents/image/pipeline/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/pipeline/execution.rb +0 -1
- data/lib/ruby_llm/agents/image/transformer/dsl.rb +0 -13
- data/lib/ruby_llm/agents/image/transformer/execution.rb +1 -10
- data/lib/ruby_llm/agents/image/transformer.rb +0 -1
- data/lib/ruby_llm/agents/image/upscaler/execution.rb +1 -2
- data/lib/ruby_llm/agents/image/variator/execution.rb +1 -2
- data/lib/ruby_llm/agents/infrastructure/alert_manager.rb +78 -173
- data/lib/ruby_llm/agents/infrastructure/attempt_tracker.rb +1 -0
- data/lib/ruby_llm/agents/infrastructure/budget/budget_query.rb +66 -2
- data/lib/ruby_llm/agents/infrastructure/budget/spend_recorder.rb +0 -12
- data/lib/ruby_llm/agents/infrastructure/circuit_breaker.rb +10 -13
- data/lib/ruby_llm/agents/infrastructure/reliability.rb +37 -2
- data/lib/ruby_llm/agents/pipeline/context.rb +0 -1
- data/lib/ruby_llm/agents/pipeline/middleware/budget.rb +28 -4
- data/lib/ruby_llm/agents/pipeline/middleware/cache.rb +3 -10
- data/lib/ruby_llm/agents/pipeline/middleware/instrumentation.rb +88 -55
- data/lib/ruby_llm/agents/pipeline/middleware/tenant.rb +5 -41
- data/lib/ruby_llm/agents/rails/engine.rb +6 -6
- data/lib/ruby_llm/agents/results/base.rb +1 -49
- data/lib/ruby_llm/agents/text/embedder.rb +0 -1
- data/lib/ruby_llm/agents.rb +1 -9
- data/lib/tasks/ruby_llm_agents.rake +34 -0
- metadata +12 -81
- data/app/controllers/ruby_llm/agents/api_configurations_controller.rb +0 -214
- data/app/controllers/ruby_llm/agents/workflows_controller.rb +0 -544
- data/app/mailers/ruby_llm/agents/alert_mailer.rb +0 -84
- data/app/mailers/ruby_llm/agents/application_mailer.rb +0 -28
- data/app/models/ruby_llm/agents/api_configuration.rb +0 -386
- data/app/models/ruby_llm/agents/execution/workflow.rb +0 -170
- data/app/models/ruby_llm/agents/tenant/configurable.rb +0 -135
- data/app/views/ruby_llm/agents/agents/_agent.html.erb +0 -98
- data/app/views/ruby_llm/agents/agents/_version_comparison.html.erb +0 -186
- data/app/views/ruby_llm/agents/agents/_workflow.html.erb +0 -126
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.html.erb +0 -107
- data/app/views/ruby_llm/agents/alert_mailer/alert_notification.text.erb +0 -18
- data/app/views/ruby_llm/agents/api_configurations/_api_key_field.html.erb +0 -34
- data/app/views/ruby_llm/agents/api_configurations/_form.html.erb +0 -288
- data/app/views/ruby_llm/agents/api_configurations/edit.html.erb +0 -95
- data/app/views/ruby_llm/agents/api_configurations/edit_tenant.html.erb +0 -97
- data/app/views/ruby_llm/agents/api_configurations/show.html.erb +0 -214
- data/app/views/ruby_llm/agents/api_configurations/tenant.html.erb +0 -179
- data/app/views/ruby_llm/agents/dashboard/_agent_comparison.html.erb +0 -73
- data/app/views/ruby_llm/agents/dashboard/_alerts_feed.html.erb +0 -62
- data/app/views/ruby_llm/agents/dashboard/_breaker_strip.html.erb +0 -47
- data/app/views/ruby_llm/agents/dashboard/_budgets_bar.html.erb +0 -75
- data/app/views/ruby_llm/agents/dashboard/_model_comparison.html.erb +0 -56
- data/app/views/ruby_llm/agents/dashboard/_model_cost_breakdown.html.erb +0 -115
- data/app/views/ruby_llm/agents/dashboard/_now_strip.html.erb +0 -59
- data/app/views/ruby_llm/agents/dashboard/_top_errors.html.erb +0 -60
- data/app/views/ruby_llm/agents/executions/_workflow_summary.html.erb +0 -86
- data/app/views/ruby_llm/agents/executions/dry_run.html.erb +0 -149
- data/app/views/ruby_llm/agents/shared/_breadcrumbs.html.erb +0 -48
- data/app/views/ruby_llm/agents/shared/_nav_link.html.erb +0 -27
- data/app/views/ruby_llm/agents/shared/_stat_card.html.erb +0 -14
- data/app/views/ruby_llm/agents/shared/_workflow_type_badge.html.erb +0 -35
- data/app/views/ruby_llm/agents/workflows/_empty_state.html.erb +0 -22
- data/app/views/ruby_llm/agents/workflows/_step_performance.html.erb +0 -228
- data/app/views/ruby_llm/agents/workflows/_structure_dsl.html.erb +0 -539
- data/app/views/ruby_llm/agents/workflows/_structure_parallel.html.erb +0 -76
- data/app/views/ruby_llm/agents/workflows/_structure_pipeline.html.erb +0 -74
- data/app/views/ruby_llm/agents/workflows/_structure_router.html.erb +0 -108
- data/app/views/ruby_llm/agents/workflows/_workflow_diagram.html.erb +0 -920
- data/app/views/ruby_llm/agents/workflows/index.html.erb +0 -179
- data/app/views/ruby_llm/agents/workflows/show.html.erb +0 -467
- data/lib/generators/ruby_llm_agents/api_configuration_generator.rb +0 -100
- data/lib/generators/ruby_llm_agents/templates/add_workflow_migration.rb.tt +0 -38
- data/lib/generators/ruby_llm_agents/templates/application_workflow.rb.tt +0 -48
- data/lib/generators/ruby_llm_agents/templates/create_api_configurations_migration.rb.tt +0 -90
- data/lib/generators/ruby_llm_agents/templates/skills/WORKFLOWS.md.tt +0 -551
- data/lib/ruby_llm/agents/core/base/moderation_dsl.rb +0 -181
- data/lib/ruby_llm/agents/core/base/moderation_execution.rb +0 -274
- data/lib/ruby_llm/agents/core/resolved_config.rb +0 -348
- data/lib/ruby_llm/agents/image/generator/content_policy.rb +0 -95
- data/lib/ruby_llm/agents/infrastructure/redactor.rb +0 -130
- data/lib/ruby_llm/agents/results/moderation_result.rb +0 -158
- data/lib/ruby_llm/agents/text/moderator.rb +0 -237
- data/lib/ruby_llm/agents/workflow/approval.rb +0 -205
- data/lib/ruby_llm/agents/workflow/approval_store.rb +0 -179
- data/lib/ruby_llm/agents/workflow/async.rb +0 -220
- data/lib/ruby_llm/agents/workflow/async_executor.rb +0 -156
- data/lib/ruby_llm/agents/workflow/dsl/executor.rb +0 -467
- data/lib/ruby_llm/agents/workflow/dsl/input_schema.rb +0 -244
- data/lib/ruby_llm/agents/workflow/dsl/iteration_executor.rb +0 -289
- data/lib/ruby_llm/agents/workflow/dsl/parallel_group.rb +0 -107
- data/lib/ruby_llm/agents/workflow/dsl/route_builder.rb +0 -150
- data/lib/ruby_llm/agents/workflow/dsl/schedule_helpers.rb +0 -187
- data/lib/ruby_llm/agents/workflow/dsl/step_config.rb +0 -352
- data/lib/ruby_llm/agents/workflow/dsl/step_executor.rb +0 -415
- data/lib/ruby_llm/agents/workflow/dsl/wait_config.rb +0 -257
- data/lib/ruby_llm/agents/workflow/dsl/wait_executor.rb +0 -317
- data/lib/ruby_llm/agents/workflow/dsl.rb +0 -576
- data/lib/ruby_llm/agents/workflow/instrumentation.rb +0 -249
- data/lib/ruby_llm/agents/workflow/notifiers/base.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/email.rb +0 -117
- data/lib/ruby_llm/agents/workflow/notifiers/slack.rb +0 -180
- data/lib/ruby_llm/agents/workflow/notifiers/webhook.rb +0 -121
- data/lib/ruby_llm/agents/workflow/notifiers.rb +0 -70
- data/lib/ruby_llm/agents/workflow/orchestrator.rb +0 -416
- data/lib/ruby_llm/agents/workflow/result.rb +0 -592
- data/lib/ruby_llm/agents/workflow/thread_pool.rb +0 -185
- data/lib/ruby_llm/agents/workflow/throttle_manager.rb +0 -206
- 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
|
-
<!--
|
|
14
|
-
<div class="
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
<div class="flex items-center
|
|
18
|
-
|
|
19
|
-
<
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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">·</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">·</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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
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
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
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
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
-
|
|
279
|
-
|
|
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
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
331
|
-
|
|
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">—</div>
|
|
201
|
+
<% end %>
|
|
202
|
+
</div>
|
|
332
203
|
|
|
333
|
-
<!--
|
|
334
|
-
<div
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
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] %>×</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
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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="
|
|
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">—</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>
|