ruby_llm-agents 0.3.1 → 0.3.4

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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +88 -0
  3. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +68 -4
  4. data/app/models/ruby_llm/agents/execution/analytics.rb +114 -13
  5. data/app/models/ruby_llm/agents/execution/scopes.rb +10 -0
  6. data/app/models/ruby_llm/agents/execution.rb +26 -58
  7. data/app/views/layouts/rubyllm/agents/application.html.erb +103 -352
  8. data/app/views/rubyllm/agents/agents/_agent.html.erb +87 -0
  9. data/app/views/rubyllm/agents/agents/index.html.erb +2 -71
  10. data/app/views/rubyllm/agents/agents/show.html.erb +349 -416
  11. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +7 -7
  12. data/app/views/rubyllm/agents/dashboard/_agent_comparison.html.erb +46 -0
  13. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +0 -90
  14. data/app/views/rubyllm/agents/dashboard/_execution_item.html.erb +54 -39
  15. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +79 -5
  16. data/app/views/rubyllm/agents/dashboard/_top_errors.html.erb +49 -0
  17. data/app/views/rubyllm/agents/dashboard/index.html.erb +76 -151
  18. data/app/views/rubyllm/agents/executions/show.html.erb +256 -93
  19. data/app/views/rubyllm/agents/settings/show.html.erb +1 -1
  20. data/app/views/rubyllm/agents/shared/_breadcrumbs.html.erb +48 -0
  21. data/app/views/rubyllm/agents/shared/_nav_link.html.erb +27 -0
  22. data/config/routes.rb +2 -0
  23. data/lib/generators/ruby_llm_agents/templates/add_tool_calls_migration.rb.tt +28 -0
  24. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +7 -0
  25. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +13 -0
  26. data/lib/ruby_llm/agents/base/caching.rb +43 -0
  27. data/lib/ruby_llm/agents/base/cost_calculation.rb +103 -0
  28. data/lib/ruby_llm/agents/base/dsl.rb +261 -0
  29. data/lib/ruby_llm/agents/base/execution.rb +206 -0
  30. data/lib/ruby_llm/agents/base/reliability_execution.rb +131 -0
  31. data/lib/ruby_llm/agents/base/response_building.rb +86 -0
  32. data/lib/ruby_llm/agents/base/tool_tracking.rb +57 -0
  33. data/lib/ruby_llm/agents/base.rb +19 -619
  34. data/lib/ruby_llm/agents/instrumentation.rb +36 -3
  35. data/lib/ruby_llm/agents/result.rb +235 -0
  36. data/lib/ruby_llm/agents/version.rb +1 -1
  37. data/lib/ruby_llm/agents.rb +1 -0
  38. metadata +15 -20
  39. data/app/channels/ruby_llm/agents/executions_channel.rb +0 -46
  40. data/app/javascript/ruby_llm/agents/controllers/filter_controller.js +0 -56
  41. data/app/javascript/ruby_llm/agents/controllers/index.js +0 -12
  42. data/app/javascript/ruby_llm/agents/controllers/refresh_controller.js +0 -83
  43. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +0 -71
@@ -9,11 +9,11 @@
9
9
 
10
10
  <div class="space-y-2">
11
11
  <% critical_alerts.each do |alert| %>
12
- <div class="flex items-center justify-between bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm">
12
+ <div class="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-2 sm:gap-0 bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm">
13
13
  <% case alert[:type] %>
14
14
  <% when :breaker %>
15
15
  <div class="flex items-center">
16
- <span class="w-2 h-2 bg-orange-500 rounded-full mr-3"></span>
16
+ <span class="w-2 h-2 bg-orange-500 rounded-full mr-3 flex-shrink-0"></span>
17
17
  <div>
18
18
  <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
19
19
  Circuit breaker open: <%= alert[:data][:agent_type].gsub(/Agent$/, '') %>
@@ -23,13 +23,13 @@
23
23
  </p>
24
24
  </div>
25
25
  </div>
26
- <span class="text-xs text-orange-600 dark:text-orange-400 font-medium">
26
+ <span class="text-xs text-orange-600 dark:text-orange-400 font-medium ml-5 sm:ml-0">
27
27
  <%= alert[:data][:cooldown_remaining] %>s remaining
28
28
  </span>
29
29
 
30
30
  <% when :budget_breach %>
31
31
  <div class="flex items-center">
32
- <span class="w-2 h-2 bg-red-500 rounded-full mr-3"></span>
32
+ <span class="w-2 h-2 bg-red-500 rounded-full mr-3 flex-shrink-0"></span>
33
33
  <div>
34
34
  <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
35
35
  <%= alert[:data][:period].to_s.capitalize %> budget exceeded
@@ -39,11 +39,11 @@
39
39
  </p>
40
40
  </div>
41
41
  </div>
42
- <%= link_to "Adjust", ruby_llm_agents.settings_path, class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
42
+ <%= link_to "Adjust", ruby_llm_agents.settings_path, class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium ml-5 sm:ml-0" %>
43
43
 
44
44
  <% when :error_spike %>
45
45
  <div class="flex items-center">
46
- <span class="w-2 h-2 bg-red-500 rounded-full mr-3 animate-pulse"></span>
46
+ <span class="w-2 h-2 bg-red-500 rounded-full mr-3 flex-shrink-0 animate-pulse"></span>
47
47
  <div>
48
48
  <p class="text-sm font-medium text-gray-900 dark:text-gray-100">
49
49
  Error spike detected
@@ -53,7 +53,7 @@
53
53
  </p>
54
54
  </div>
55
55
  </div>
56
- <%= link_to "View failures", ruby_llm_agents.executions_path(status: "error"), class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium" %>
56
+ <%= link_to "View failures", ruby_llm_agents.executions_path(status: "error"), class: "text-xs text-red-600 dark:text-red-400 hover:underline font-medium ml-5 sm:ml-0" %>
57
57
  <% end %>
58
58
  </div>
59
59
  <% end %>
@@ -0,0 +1,46 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Agents</h3>
4
+ </div>
5
+
6
+ <% if agent_stats.any? %>
7
+ <div class="overflow-x-auto">
8
+ <table class="w-full text-sm">
9
+ <thead>
10
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
11
+ <th class="px-4 py-2">Agent</th>
12
+ <th class="px-4 py-2 text-right">Runs</th>
13
+ <th class="px-4 py-2 text-right">Cost</th>
14
+ <th class="px-4 py-2 text-right">Success</th>
15
+ </tr>
16
+ </thead>
17
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
18
+ <% agent_stats.first(5).each do |agent| %>
19
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
20
+ <td class="px-4 py-2">
21
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[120px]" title="<%= agent[:agent_type] %>">
22
+ <%= agent[:agent_type].to_s.demodulize %>
23
+ </span>
24
+ </td>
25
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
26
+ <%= number_with_delimiter(agent[:executions]) %>
27
+ </td>
28
+ <td class="px-4 py-2 text-right text-gray-600 dark:text-gray-300">
29
+ $<%= number_with_precision(agent[:total_cost], precision: 2) %>
30
+ </td>
31
+ <td class="px-4 py-2 text-right">
32
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium <%= agent[:success_rate] >= 95 ? 'bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300' : agent[:success_rate] >= 80 ? 'bg-yellow-100 dark:bg-yellow-900 text-yellow-700 dark:text-yellow-300' : 'bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300' %>">
33
+ <%= agent[:success_rate].round %>%
34
+ </span>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% else %>
42
+ <div class="px-4 py-8 text-center text-gray-500 dark:text-gray-400">
43
+ <p class="text-sm">No agent data yet</p>
44
+ </div>
45
+ <% end %>
46
+ </div>
@@ -71,95 +71,5 @@
71
71
  No budget limits configured
72
72
  </p>
73
73
  <% end %>
74
-
75
- <%# Forecast Section %>
76
- <% if budget_status[:forecast] %>
77
- <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
78
- <div class="flex items-center gap-2 mb-3">
79
- <svg class="w-4 h-4 text-purple-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
80
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"/>
81
- </svg>
82
- <h4 class="text-sm font-semibold text-gray-700 dark:text-gray-200">Forecast</h4>
83
- </div>
84
-
85
- <div class="grid grid-cols-1 md:grid-cols-2 gap-4">
86
- <%# Daily Forecast %>
87
- <% if budget_status[:forecast][:daily] %>
88
- <% daily_forecast = budget_status[:forecast][:daily] %>
89
- <div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
90
- <div class="flex items-center justify-between mb-2">
91
- <span class="text-xs font-medium text-gray-600 dark:text-gray-300">Daily Projection</span>
92
- <% if daily_forecast[:on_track] %>
93
- <span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-full">
94
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
95
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
96
- </svg>
97
- On Track
98
- </span>
99
- <% else %>
100
- <span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-full">
101
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
102
- <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"/>
103
- </svg>
104
- Over Budget
105
- </span>
106
- <% end %>
107
- </div>
108
- <div class="flex items-baseline gap-2">
109
- <span class="text-lg font-bold text-gray-900 dark:text-gray-100">
110
- $<%= number_with_precision(daily_forecast[:projected], precision: 2) %>
111
- </span>
112
- <span class="text-xs text-gray-500 dark:text-gray-400">
113
- / $<%= number_with_precision(daily_forecast[:limit], precision: 2) %> limit
114
- </span>
115
- </div>
116
- <div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
117
- <span>$<%= number_with_precision(daily_forecast[:rate_per_hour], precision: 4) %>/hr</span>
118
- <span class="mx-2">•</span>
119
- <span><%= daily_forecast[:hours_remaining] %> hrs remaining</span>
120
- </div>
121
- </div>
122
- <% end %>
123
-
124
- <%# Monthly Forecast %>
125
- <% if budget_status[:forecast][:monthly] %>
126
- <% monthly_forecast = budget_status[:forecast][:monthly] %>
127
- <div class="bg-gray-50 dark:bg-gray-700/50 rounded-lg p-3">
128
- <div class="flex items-center justify-between mb-2">
129
- <span class="text-xs font-medium text-gray-600 dark:text-gray-300">Monthly Projection</span>
130
- <% if monthly_forecast[:on_track] %>
131
- <span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-green-700 dark:text-green-300 bg-green-100 dark:bg-green-900/50 rounded-full">
132
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
133
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
134
- </svg>
135
- On Track
136
- </span>
137
- <% else %>
138
- <span class="inline-flex items-center gap-1 px-2 py-0.5 text-xs font-medium text-red-700 dark:text-red-300 bg-red-100 dark:bg-red-900/50 rounded-full">
139
- <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
140
- <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"/>
141
- </svg>
142
- Over Budget
143
- </span>
144
- <% end %>
145
- </div>
146
- <div class="flex items-baseline gap-2">
147
- <span class="text-lg font-bold text-gray-900 dark:text-gray-100">
148
- $<%= number_with_precision(monthly_forecast[:projected], precision: 2) %>
149
- </span>
150
- <span class="text-xs text-gray-500 dark:text-gray-400">
151
- / $<%= number_with_precision(monthly_forecast[:limit], precision: 2) %> limit
152
- </span>
153
- </div>
154
- <div class="mt-2 text-xs text-gray-500 dark:text-gray-400">
155
- <span>$<%= number_with_precision(monthly_forecast[:rate_per_day], precision: 2) %>/day</span>
156
- <span class="mx-2">•</span>
157
- <span><%= monthly_forecast[:days_remaining] %> days remaining</span>
158
- </div>
159
- </div>
160
- <% end %>
161
- </div>
162
- </div>
163
- <% end %>
164
74
  </div>
165
75
  <% end %>
@@ -1,48 +1,63 @@
1
- <div id="execution-<%= execution.id %>" class="py-3 hover:bg-gray-50 dark:hover:bg-gray-700 -mx-2 px-2 rounded-lg transition-colors">
1
+ <div id="execution-<%= execution.id %>" class="py-2.5 hover:bg-gray-50 dark:hover:bg-gray-700/50 -mx-2 px-2 rounded-lg transition-colors">
2
2
  <%= link_to ruby_llm_agents.execution_path(execution), data: { turbo: false }, class: "block" do %>
3
- <div class="flex items-start space-x-3">
4
- <!-- Timeline indicator -->
5
- <div class="flex flex-col items-center pt-1">
3
+ <!-- Row 1: Status dot + Agent + Badges + Timestamp -->
4
+ <div class="flex items-center justify-between gap-2">
5
+ <div class="flex items-center gap-2 min-w-0">
6
6
  <%= render "rubyllm/agents/shared/status_dot", status: execution.status %>
7
- </div>
8
-
9
- <!-- Content -->
10
- <div class="flex-1 min-w-0">
11
- <div class="flex items-center justify-between">
12
- <div class="flex items-center space-x-2">
13
- <p class="font-medium text-gray-900 dark:text-gray-100 text-sm">
14
- <%= execution.agent_type.gsub(/Agent$/, '') %>
15
- </p>
16
- <span class="text-xs text-gray-400 dark:text-gray-500">v<%= execution.agent_version %></span>
17
- <% if execution.status_running? %>
18
- <span class="text-xs text-blue-600 dark:text-blue-400 font-medium">Running...</span>
7
+ <span class="font-medium text-sm text-gray-900 dark:text-gray-100 truncate">
8
+ <%= execution.agent_type.gsub(/Agent$/, '') %>
9
+ </span>
10
+ <span class="hidden sm:inline text-xs text-gray-400 dark:text-gray-500">v<%= execution.agent_version %></span>
11
+ <% unless execution.status_running? %>
12
+ <div class="hidden sm:flex items-center gap-1">
13
+ <% if execution.streaming? %>
14
+ <span class="badge badge-cyan text-[10px] px-1.5 py-0.5">Stream</span>
15
+ <% end %>
16
+ <% if execution.cache_hit? %>
17
+ <span class="badge badge-purple text-[10px] px-1.5 py-0.5">Cached</span>
18
+ <% end %>
19
+ <% if execution.rate_limited? %>
20
+ <span class="badge badge-orange text-[10px] px-1.5 py-0.5">Limited</span>
19
21
  <% end %>
20
22
  </div>
21
- <p class="text-xs text-gray-400 dark:text-gray-500">
22
- <%= time_ago_in_words(execution.created_at) %> ago
23
- </p>
24
- </div>
25
-
26
- <% if execution.status_running? %>
27
- <p class="text-xs text-blue-500 dark:text-blue-400 mt-1">
28
- In progress...
29
- </p>
30
- <% else %>
31
- <p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
32
- <%= number_to_human_short(execution.total_tokens || 0) %> tokens
33
- <span class="text-gray-300 dark:text-gray-600 mx-1">&middot;</span>
34
- <%= number_to_human_short(execution.total_cost || 0, prefix: "$", precision: 2) %>
35
- <span class="text-gray-300 dark:text-gray-600 mx-1">&middot;</span>
36
- <%= number_to_human_short(execution.duration_ms || 0) %>ms
37
- </p>
38
- <% end %>
39
-
40
- <% if execution.status_error? && execution.error_message.present? %>
41
- <p class="text-xs text-red-600 dark:text-red-400 mt-1.5 bg-red-50 dark:bg-red-900/30 rounded px-2 py-1">
42
- <%= execution.error_class %>: <%= truncate(execution.error_message, length: 80) %>
43
- </p>
44
23
  <% end %>
45
24
  </div>
25
+ <span class="text-xs text-gray-400 dark:text-gray-500 whitespace-nowrap flex-shrink-0">
26
+ <%= time_ago_in_words(execution.created_at) %> ago
27
+ </span>
46
28
  </div>
29
+
30
+ <!-- Row 2: Model + Metrics -->
31
+ <div class="mt-1 text-xs text-gray-500 dark:text-gray-400">
32
+ <% if execution.status_running? %>
33
+ <span class="text-blue-500 dark:text-blue-400 animate-pulse">Processing...</span>
34
+ <% else %>
35
+ <!-- Mobile: compact -->
36
+ <span class="sm:hidden">
37
+ <%= number_to_human_short(execution.total_tokens || 0) %> tokens · $<%= number_with_precision(execution.total_cost || 0, precision: 2) %>
38
+ </span>
39
+ <!-- Desktop: full -->
40
+ <span class="hidden sm:inline">
41
+ <span class="font-mono text-gray-400 dark:text-gray-500"><%= execution.model_id %></span>
42
+ <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
43
+ <%= number_to_human_short(execution.total_tokens || 0) %> tokens
44
+ <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
45
+ $<%= number_with_precision(execution.total_cost || 0, precision: 4) %>
46
+ <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
47
+ <%= number_to_human_short(execution.duration_ms || 0) %>ms
48
+ <% if execution.attempts_count && execution.attempts_count > 1 %>
49
+ <span class="mx-1.5 text-gray-300 dark:text-gray-600">·</span>
50
+ <span class="text-amber-600 dark:text-amber-400"><%= execution.attempts_count %> retries</span>
51
+ <% end %>
52
+ </span>
53
+ <% end %>
54
+ </div>
55
+
56
+ <!-- Error display -->
57
+ <% if execution.status_error? && execution.error_message.present? %>
58
+ <p class="mt-1.5 text-xs text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-900/30 rounded px-2 py-1">
59
+ <%= execution.error_class %>: <%= truncate(execution.error_message, length: 80) %>
60
+ </p>
61
+ <% end %>
47
62
  <% end %>
48
63
  </div>
@@ -1,10 +1,84 @@
1
1
  <div class="mb-6">
2
- <div class="flex items-center mb-3">
3
- <span class="w-2 h-2 bg-green-500 rounded-full mr-2 animate-pulse"></span>
4
- <h3 class="text-sm font-medium text-gray-600 dark:text-gray-400">Live</h3>
2
+ <div class="flex items-center justify-end mb-3">
3
+ <div class="flex space-x-1 bg-gray-100 dark:bg-gray-700 rounded-lg p-1">
4
+ <%= link_to "Today", ruby_llm_agents.root_path(range: "today"),
5
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == 'today' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
6
+ <%= link_to "7 Days", ruby_llm_agents.root_path(range: "7d"),
7
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == '7d' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
8
+ <%= link_to "30 Days", ruby_llm_agents.root_path(range: "30d"),
9
+ class: "px-3 py-1 text-xs font-medium rounded-md transition-colors #{@selected_range == '30d' ? 'bg-white dark:bg-gray-600 text-gray-900 dark:text-white shadow-sm' : 'text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white'}" %>
10
+ </div>
5
11
  </div>
6
12
 
7
- <div id="now-strip-values">
8
- <%= render partial: "rubyllm/agents/dashboard/now_strip_values", locals: { now_strip: now_strip } %>
13
+ <div class="grid grid-cols-2 sm:grid-cols-3 lg:grid-cols-6 gap-3">
14
+ <!-- Running -->
15
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
16
+ <div class="flex-shrink-0 w-8 h-8 bg-blue-100 dark:bg-blue-900 rounded-full flex items-center justify-center mr-3">
17
+ <span class="w-2.5 h-2.5 bg-blue-500 rounded-full animate-pulse"></span>
18
+ </div>
19
+ <div>
20
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100"><%= now_strip[:running] %></p>
21
+ <p class="text-xs text-gray-500 dark:text-gray-400">Running</p>
22
+ </div>
23
+ </div>
24
+
25
+ <!-- Success -->
26
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
27
+ <div class="flex-shrink-0 w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-3">
28
+ <span class="w-2.5 h-2.5 bg-green-500 rounded-full"></span>
29
+ </div>
30
+ <div>
31
+ <p class="text-lg font-bold text-green-600 dark:text-green-400"><%= now_strip[:success_today] %></p>
32
+ <p class="text-xs text-gray-500 dark:text-gray-400">Success</p>
33
+ </div>
34
+ </div>
35
+
36
+ <!-- Errors -->
37
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
38
+ <div class="flex-shrink-0 w-8 h-8 <%= now_strip[:errors_today] > 0 ? 'bg-red-100 dark:bg-red-900' : 'bg-gray-100 dark:bg-gray-700' %> rounded-full flex items-center justify-center mr-3">
39
+ <span class="w-2.5 h-2.5 <%= now_strip[:errors_today] > 0 ? 'bg-red-500' : 'bg-gray-400' %> rounded-full"></span>
40
+ </div>
41
+ <div>
42
+ <p class="text-lg font-bold <%= now_strip[:errors_today] > 0 ? 'text-red-600 dark:text-red-400' : 'text-gray-900 dark:text-gray-100' %>"><%= now_strip[:errors_today] %></p>
43
+ <p class="text-xs text-gray-500 dark:text-gray-400">Errors</p>
44
+ </div>
45
+ </div>
46
+
47
+ <!-- Timeouts -->
48
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
49
+ <div class="flex-shrink-0 w-8 h-8 <%= now_strip[:timeouts_today] > 0 ? 'bg-yellow-100 dark:bg-yellow-900' : 'bg-gray-100 dark:bg-gray-700' %> rounded-full flex items-center justify-center mr-3">
50
+ <span class="w-2.5 h-2.5 <%= now_strip[:timeouts_today] > 0 ? 'bg-yellow-500' : 'bg-gray-400' %> rounded-full"></span>
51
+ </div>
52
+ <div>
53
+ <p class="text-lg font-bold <%= now_strip[:timeouts_today] > 0 ? 'text-yellow-600 dark:text-yellow-400' : 'text-gray-900 dark:text-gray-100' %>"><%= now_strip[:timeouts_today] %></p>
54
+ <p class="text-xs text-gray-500 dark:text-gray-400">Timeouts</p>
55
+ </div>
56
+ </div>
57
+
58
+ <!-- Cost -->
59
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
60
+ <div class="flex-shrink-0 w-8 h-8 bg-amber-100 dark:bg-amber-900 rounded-full flex items-center justify-center mr-3">
61
+ <svg class="w-4 h-4 text-amber-600 dark:text-amber-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
62
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8c-1.657 0-3 .895-3 2s1.343 2 3 2 3 .895 3 2-1.343 2-3 2m0-8c1.11 0 2.08.402 2.599 1M12 8V7m0 1v8m0 0v1m0-1c-1.11 0-2.08-.402-2.599-1M21 12a9 9 0 11-18 0 9 9 0 0118 0z"/>
63
+ </svg>
64
+ </div>
65
+ <div>
66
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100">$<%= number_with_precision(now_strip[:cost_today], precision: 4) %></p>
67
+ <p class="text-xs text-gray-500 dark:text-gray-400">Cost</p>
68
+ </div>
69
+ </div>
70
+
71
+ <!-- Success Rate -->
72
+ <div class="flex items-center bg-white dark:bg-gray-800 rounded-lg px-4 py-3 shadow-sm border border-gray-100 dark:border-gray-700">
73
+ <div class="flex-shrink-0 w-8 h-8 bg-green-100 dark:bg-green-900 rounded-full flex items-center justify-center mr-3">
74
+ <svg class="w-4 h-4 text-green-600 dark:text-green-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
76
+ </svg>
77
+ </div>
78
+ <div>
79
+ <p class="text-lg font-bold text-gray-900 dark:text-gray-100"><%= now_strip[:success_rate] %>%</p>
80
+ <p class="text-xs text-gray-500 dark:text-gray-400">Success Rate</p>
81
+ </div>
82
+ </div>
9
83
  </div>
10
84
  </div>
@@ -0,0 +1,49 @@
1
+ <div class="bg-white dark:bg-gray-800 rounded-xl shadow-sm border border-gray-100 dark:border-gray-700 h-full">
2
+ <div class="px-4 py-3 border-b border-gray-100 dark:border-gray-700">
3
+ <h3 class="text-base font-semibold text-gray-900 dark:text-gray-100">Top Errors</h3>
4
+ </div>
5
+
6
+ <% if top_errors.any? %>
7
+ <div class="overflow-x-auto">
8
+ <table class="w-full text-sm">
9
+ <thead>
10
+ <tr class="text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
11
+ <th class="px-4 py-2">Error</th>
12
+ <th class="px-4 py-2 text-right">Count</th>
13
+ <th class="px-4 py-2 text-right">Last Seen</th>
14
+ </tr>
15
+ </thead>
16
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
17
+ <% top_errors.first(5).each do |error| %>
18
+ <tr class="hover:bg-gray-50 dark:hover:bg-gray-700/50">
19
+ <td class="px-4 py-2">
20
+ <span class="text-sm font-medium text-gray-900 dark:text-gray-100 truncate block max-w-[180px]" title="<%= error[:error_class] %>">
21
+ <%= error[:error_class].to_s.split("::").last %>
22
+ </span>
23
+ </td>
24
+ <td class="px-4 py-2 text-right">
25
+ <span class="inline-flex items-center px-1.5 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900 text-red-700 dark:text-red-300">
26
+ <%= number_with_delimiter(error[:count]) %>
27
+ </span>
28
+ </td>
29
+ <td class="px-4 py-2 text-right text-xs text-gray-500 dark:text-gray-400">
30
+ <% if error[:last_seen] %>
31
+ <%= time_ago_in_words(error[:last_seen]) %>
32
+ <% else %>
33
+ -
34
+ <% end %>
35
+ </td>
36
+ </tr>
37
+ <% end %>
38
+ </tbody>
39
+ </table>
40
+ </div>
41
+ <% else %>
42
+ <div class="px-4 py-8 text-center text-green-500 dark:text-green-400">
43
+ <svg class="w-8 h-8 mx-auto mb-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
44
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
45
+ </svg>
46
+ <p class="text-sm">No errors</p>
47
+ </div>
48
+ <% end %>
49
+ </div>