ruby_llm-agents 0.2.4 → 0.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (62) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +413 -0
  3. data/app/channels/ruby_llm/agents/executions_channel.rb +24 -1
  4. data/app/controllers/concerns/ruby_llm/agents/filterable.rb +81 -0
  5. data/app/controllers/concerns/ruby_llm/agents/paginatable.rb +51 -0
  6. data/app/controllers/ruby_llm/agents/agents_controller.rb +228 -59
  7. data/app/controllers/ruby_llm/agents/dashboard_controller.rb +167 -12
  8. data/app/controllers/ruby_llm/agents/executions_controller.rb +189 -31
  9. data/app/controllers/ruby_llm/agents/settings_controller.rb +20 -0
  10. data/app/helpers/ruby_llm/agents/application_helper.rb +307 -7
  11. data/app/models/ruby_llm/agents/execution/analytics.rb +224 -20
  12. data/app/models/ruby_llm/agents/execution/metrics.rb +41 -25
  13. data/app/models/ruby_llm/agents/execution/scopes.rb +234 -14
  14. data/app/models/ruby_llm/agents/execution.rb +259 -16
  15. data/app/services/ruby_llm/agents/agent_registry.rb +49 -12
  16. data/app/views/layouts/rubyllm/agents/application.html.erb +351 -85
  17. data/app/views/rubyllm/agents/agents/_version_comparison.html.erb +186 -0
  18. data/app/views/rubyllm/agents/agents/show.html.erb +233 -10
  19. data/app/views/rubyllm/agents/dashboard/_action_center.html.erb +62 -0
  20. data/app/views/rubyllm/agents/dashboard/_alerts_feed.html.erb +62 -0
  21. data/app/views/rubyllm/agents/dashboard/_breaker_strip.html.erb +47 -0
  22. data/app/views/rubyllm/agents/dashboard/_budgets_bar.html.erb +165 -0
  23. data/app/views/rubyllm/agents/dashboard/_now_strip.html.erb +10 -0
  24. data/app/views/rubyllm/agents/dashboard/_now_strip_values.html.erb +71 -0
  25. data/app/views/rubyllm/agents/dashboard/index.html.erb +215 -109
  26. data/app/views/rubyllm/agents/executions/_filters.html.erb +152 -155
  27. data/app/views/rubyllm/agents/executions/_list.html.erb +103 -12
  28. data/app/views/rubyllm/agents/executions/dry_run.html.erb +149 -0
  29. data/app/views/rubyllm/agents/executions/index.html.erb +17 -72
  30. data/app/views/rubyllm/agents/executions/index.turbo_stream.erb +16 -2
  31. data/app/views/rubyllm/agents/executions/show.html.erb +693 -14
  32. data/app/views/rubyllm/agents/settings/show.html.erb +369 -0
  33. data/app/views/rubyllm/agents/shared/_filter_dropdown.html.erb +121 -0
  34. data/app/views/rubyllm/agents/shared/_select_dropdown.html.erb +85 -0
  35. data/config/routes.rb +7 -0
  36. data/lib/generators/ruby_llm_agents/templates/add_attempts_migration.rb.tt +27 -0
  37. data/lib/generators/ruby_llm_agents/templates/add_caching_migration.rb.tt +23 -0
  38. data/lib/generators/ruby_llm_agents/templates/add_finish_reason_migration.rb.tt +19 -0
  39. data/lib/generators/ruby_llm_agents/templates/add_routing_migration.rb.tt +19 -0
  40. data/lib/generators/ruby_llm_agents/templates/add_streaming_migration.rb.tt +8 -0
  41. data/lib/generators/ruby_llm_agents/templates/add_tracing_migration.rb.tt +34 -0
  42. data/lib/generators/ruby_llm_agents/templates/agent.rb.tt +66 -4
  43. data/lib/generators/ruby_llm_agents/templates/application_agent.rb.tt +53 -6
  44. data/lib/generators/ruby_llm_agents/templates/initializer.rb.tt +143 -8
  45. data/lib/generators/ruby_llm_agents/templates/migration.rb.tt +38 -1
  46. data/lib/generators/ruby_llm_agents/upgrade_generator.rb +78 -0
  47. data/lib/ruby_llm/agents/alert_manager.rb +207 -0
  48. data/lib/ruby_llm/agents/attempt_tracker.rb +295 -0
  49. data/lib/ruby_llm/agents/base.rb +597 -112
  50. data/lib/ruby_llm/agents/budget_tracker.rb +360 -0
  51. data/lib/ruby_llm/agents/circuit_breaker.rb +197 -0
  52. data/lib/ruby_llm/agents/configuration.rb +279 -1
  53. data/lib/ruby_llm/agents/engine.rb +58 -6
  54. data/lib/ruby_llm/agents/execution_logger_job.rb +17 -6
  55. data/lib/ruby_llm/agents/inflections.rb +13 -2
  56. data/lib/ruby_llm/agents/instrumentation.rb +538 -87
  57. data/lib/ruby_llm/agents/redactor.rb +130 -0
  58. data/lib/ruby_llm/agents/reliability.rb +185 -0
  59. data/lib/ruby_llm/agents/version.rb +3 -1
  60. data/lib/ruby_llm/agents.rb +52 -0
  61. metadata +41 -2
  62. data/app/controllers/ruby_llm/agents/application_controller.rb +0 -37
@@ -1,3 +1,4 @@
1
+ <div id="execution-detail" data-execution-id="<%= @execution.id %>" data-status="<%= @execution.status %>">
1
2
  <div class="mb-6">
2
3
  <%= link_to ruby_llm_agents.executions_path, class: "inline-flex items-center text-sm text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200" do %>
3
4
  <svg
@@ -27,19 +28,113 @@
27
28
  </h2>
28
29
 
29
30
  <%= render "rubyllm/agents/shared/status_badge", status: @execution.status, size: :md %>
31
+
32
+ <% if @execution.streaming? %>
33
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-cyan-100 dark:bg-cyan-900/50 text-cyan-800 dark:text-cyan-300">
34
+ Streaming
35
+ </span>
36
+ <% end %>
37
+
38
+ <% if @execution.cache_hit %>
39
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-purple-100 dark:bg-purple-900/50 text-purple-800 dark:text-purple-300">
40
+ Cached
41
+ </span>
42
+ <% end %>
43
+
44
+ <% if @execution.finish_reason.present? %>
45
+ <% finish_colors = {
46
+ 'stop' => 'bg-green-100 text-green-800 dark:bg-green-900/50 dark:text-green-300',
47
+ 'length' => 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900/50 dark:text-yellow-300',
48
+ 'content_filter' => 'bg-red-100 text-red-800 dark:bg-red-900/50 dark:text-red-300',
49
+ 'tool_calls' => 'bg-blue-100 text-blue-800 dark:bg-blue-900/50 dark:text-blue-300'
50
+ } %>
51
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium <%= finish_colors[@execution.finish_reason] || 'bg-gray-100 text-gray-800 dark:bg-gray-700 dark:text-gray-300' %>">
52
+ <%= @execution.finish_reason %>
53
+ </span>
54
+ <% end %>
30
55
  </div>
31
56
 
32
57
  <p class="text-sm text-gray-500 dark:text-gray-400 mt-1">
33
58
  Execution #<%= @execution.id %> · v<%= @execution.agent_version %>
59
+ <% if @execution.model_provider.present? %>
60
+ · <%= @execution.model_provider %>
61
+ <% end %>
34
62
  </p>
35
63
  </div>
36
64
 
37
- <div class="text-right text-sm text-gray-500 dark:text-gray-400">
38
- <p><%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %></p>
65
+ <div class="flex items-center gap-4">
66
+ <!-- Rerun Buttons -->
67
+ <div class="flex items-center gap-2">
68
+ <%= button_to rerun_execution_path(@execution, dry_run: true),
69
+ method: :post,
70
+ data: { turbo: false },
71
+ class: "inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-50 dark:hover:bg-gray-600 transition-colors",
72
+ title: "Preview what would be sent without making an API call" do %>
73
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
74
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
76
+ </svg>
77
+ Dry Run
78
+ <% end %>
39
79
 
40
- <p class="text-xs text-gray-400 dark:text-gray-500">
41
- <%= time_ago_in_words(@execution.created_at) %> ago
42
- </p>
80
+ <button
81
+ type="button"
82
+ onclick="confirmRerun()"
83
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-lg transition-colors"
84
+ title="Re-execute this agent with the same parameters"
85
+ >
86
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
87
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
88
+ </svg>
89
+ Rerun
90
+ </button>
91
+ </div>
92
+
93
+ <div class="text-right text-sm text-gray-500 dark:text-gray-400">
94
+ <p><%= @execution.created_at.strftime("%b %d, %Y at %H:%M") %></p>
95
+
96
+ <p class="text-xs text-gray-400 dark:text-gray-500">
97
+ <%= time_ago_in_words(@execution.created_at) %> ago
98
+ </p>
99
+ </div>
100
+ </div>
101
+ </div>
102
+ </div>
103
+
104
+ <!-- Rerun Confirmation Modal -->
105
+ <div id="rerun-modal" class="hidden fixed inset-0 z-50 overflow-y-auto">
106
+ <div class="flex items-center justify-center min-h-screen px-4 pt-4 pb-20 text-center sm:p-0">
107
+ <div class="fixed inset-0 transition-opacity bg-gray-500 bg-opacity-75 dark:bg-gray-900 dark:bg-opacity-75" onclick="closeRerunModal()"></div>
108
+
109
+ <div class="relative inline-block px-4 pt-5 pb-4 overflow-hidden text-left align-bottom transition-all transform bg-white dark:bg-gray-800 rounded-lg shadow-xl sm:my-8 sm:align-middle sm:max-w-lg sm:w-full sm:p-6">
110
+ <div class="sm:flex sm:items-start">
111
+ <div class="flex items-center justify-center flex-shrink-0 w-12 h-12 mx-auto bg-blue-100 dark:bg-blue-900/30 rounded-full sm:mx-0 sm:h-10 sm:w-10">
112
+ <svg class="w-6 h-6 text-blue-600 dark:text-blue-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15"/>
114
+ </svg>
115
+ </div>
116
+ <div class="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
117
+ <h3 class="text-lg font-medium leading-6 text-gray-900 dark:text-gray-100">Confirm Rerun</h3>
118
+ <div class="mt-2">
119
+ <p class="text-sm text-gray-500 dark:text-gray-400">
120
+ This will re-execute the agent with the original parameters. A new execution record will be created and the agent will make a real API call.
121
+ </p>
122
+ <p class="mt-2 text-sm text-amber-600 dark:text-amber-400">
123
+ This action may incur API costs.
124
+ </p>
125
+ </div>
126
+ </div>
127
+ </div>
128
+ <div class="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse gap-3">
129
+ <%= button_to rerun_execution_path(@execution),
130
+ method: :post,
131
+ class: "inline-flex justify-center w-full px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 sm:w-auto" do %>
132
+ Confirm Rerun
133
+ <% end %>
134
+ <button type="button" onclick="closeRerunModal()" class="inline-flex justify-center w-full px-4 py-2 mt-3 text-sm font-medium text-gray-700 dark:text-gray-200 bg-white dark:bg-gray-700 border border-gray-300 dark:border-gray-600 rounded-md shadow-sm hover:bg-gray-50 dark:hover:bg-gray-600 focus:outline-none sm:mt-0 sm:w-auto">
135
+ Cancel
136
+ </button>
137
+ </div>
43
138
  </div>
44
139
  </div>
45
140
  </div>
@@ -125,10 +220,135 @@
125
220
  </div>
126
221
  </div>
127
222
 
223
+ <!-- Attempts Table (for reliability-enabled executions) -->
224
+ <% if @execution.respond_to?(:attempts) && @execution.attempts.present? %>
225
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
226
+ <div class="flex items-center justify-between mb-4">
227
+ <div>
228
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Attempts</h3>
229
+ <p class="text-xs text-gray-400 dark:text-gray-500 mt-1">
230
+ <%= @execution.attempts_count || @execution.attempts.size %> attempt(s)
231
+ <% if @execution.used_fallback? %>
232
+ · <span class="text-amber-500">Used fallback model</span>
233
+ <% end %>
234
+ <% if @execution.has_retries? %>
235
+ · <span class="text-blue-500">Retried</span>
236
+ <% end %>
237
+ </p>
238
+ </div>
239
+ <% if @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
240
+ <span class="inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-medium bg-amber-100 dark:bg-amber-900/30 text-amber-800 dark:text-amber-300">
241
+ Fallback: <%= @execution.chosen_model_id %>
242
+ </span>
243
+ <% end %>
244
+ </div>
245
+
246
+ <div class="overflow-x-auto">
247
+ <table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
248
+ <thead>
249
+ <tr>
250
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">#</th>
251
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Model</th>
252
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Status</th>
253
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Duration</th>
254
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
255
+ <th class="px-3 py-2 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Error</th>
256
+ </tr>
257
+ </thead>
258
+ <tbody class="divide-y divide-gray-100 dark:divide-gray-700">
259
+ <% @execution.attempts.each_with_index do |attempt, index| %>
260
+ <tr class="<%= attempt['short_circuited'] ? 'bg-gray-50 dark:bg-gray-900/50' : '' %>">
261
+ <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
262
+ <%= index + 1 %>
263
+ </td>
264
+ <td class="px-3 py-2 text-sm font-mono text-gray-900 dark:text-gray-100">
265
+ <%= attempt['model_id'] %>
266
+ </td>
267
+ <td class="px-3 py-2">
268
+ <% if attempt['short_circuited'] %>
269
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300">
270
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
271
+ <path fill-rule="evenodd" d="M13.477 14.89A6 6 0 015.11 6.524l8.367 8.368zm1.414-1.414L6.524 5.11a6 6 0 018.367 8.367zM18 10a8 8 0 11-16 0 8 8 0 0116 0z" clip-rule="evenodd"/>
272
+ </svg>
273
+ Blocked
274
+ </span>
275
+ <% elsif attempt['error_class'].present? %>
276
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-300">
277
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
278
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/>
279
+ </svg>
280
+ Failed
281
+ </span>
282
+ <% else %>
283
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-300">
284
+ <svg class="w-3 h-3 mr-1" fill="currentColor" viewBox="0 0 20 20">
285
+ <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/>
286
+ </svg>
287
+ Success
288
+ </span>
289
+ <% end %>
290
+ </td>
291
+ <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
292
+ <%= attempt['duration_ms'] ? "#{attempt['duration_ms']}ms" : '-' %>
293
+ </td>
294
+ <td class="px-3 py-2 text-sm text-gray-500 dark:text-gray-400">
295
+ <% if attempt['input_tokens'] || attempt['output_tokens'] %>
296
+ <span class="text-blue-600 dark:text-blue-400"><%= attempt['input_tokens'] || 0 %></span>
297
+ /
298
+ <span class="text-green-600 dark:text-green-400"><%= attempt['output_tokens'] || 0 %></span>
299
+ <% else %>
300
+ -
301
+ <% end %>
302
+ </td>
303
+ <td class="px-3 py-2 text-sm">
304
+ <% if attempt['error_class'].present? %>
305
+ <span class="text-red-600 dark:text-red-400 font-mono text-xs" title="<%= attempt['error_message'] %>">
306
+ <%= attempt['error_class'].split('::').last.truncate(30) %>
307
+ </span>
308
+ <% else %>
309
+ <span class="text-gray-400">-</span>
310
+ <% end %>
311
+ </td>
312
+ </tr>
313
+ <% end %>
314
+ </tbody>
315
+ </table>
316
+ </div>
317
+
318
+ <% if @execution.fallback_chain.present? && @execution.fallback_chain.any? %>
319
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
320
+ <p class="text-xs text-gray-500 dark:text-gray-400">
321
+ <span class="font-medium">Fallback chain:</span>
322
+ <%= @execution.fallback_chain.join(' → ') %>
323
+ </p>
324
+ <% if @execution.fallback_reason.present? %>
325
+ <p class="text-xs text-amber-600 dark:text-amber-400 mt-1">
326
+ Fallback reason: <%= @execution.fallback_reason %>
327
+ </p>
328
+ <% end %>
329
+ </div>
330
+ <% end %>
331
+ </div>
332
+ <% end %>
333
+
128
334
  <% if @execution.status_error? %>
129
335
  <!-- Error Details -->
130
336
  <div class="bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-800 rounded-lg p-6 mb-6">
131
- <h3 class="text-lg font-semibold text-red-800 dark:text-red-300 mb-2">Error Details</h3>
337
+ <div class="flex items-center justify-between mb-2">
338
+ <h3 class="text-lg font-semibold text-red-800 dark:text-red-300">Error Details</h3>
339
+ <div class="flex items-center gap-2">
340
+ <% if @execution.rate_limited? %>
341
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-orange-100 text-orange-700 dark:bg-orange-900/30 dark:text-orange-300">
342
+ Rate Limited
343
+ </span>
344
+ <% end %>
345
+ <% if @execution.retryable %>
346
+ <span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-300">
347
+ Retryable
348
+ </span>
349
+ <% end %>
350
+ </div>
351
+ </div>
132
352
 
133
353
  <p class="font-mono text-sm text-red-700 dark:text-red-400 mb-2">
134
354
  <%= @execution.error_class %>
@@ -138,13 +358,46 @@
138
358
  </div>
139
359
  <% end %>
140
360
 
361
+ <!-- Masking Toggle -->
362
+ <div class="bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 rounded-xl p-4 mb-6">
363
+ <div class="flex items-center justify-between">
364
+ <div class="flex items-center gap-2">
365
+ <svg class="w-5 h-5 text-amber-500" fill="none" stroke="currentColor" viewBox="0 0 24 24">
366
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z"/>
367
+ </svg>
368
+ <span class="text-sm font-medium text-amber-800 dark:text-amber-200">Data Masking</span>
369
+ </div>
370
+ <div class="flex items-center gap-3">
371
+ <span id="masking-status" class="text-xs text-amber-600 dark:text-amber-400">
372
+ Sensitive data is hidden
373
+ </span>
374
+ <button
375
+ type="button"
376
+ id="toggle-masking-btn"
377
+ onclick="toggleMasking()"
378
+ class="inline-flex items-center gap-1.5 px-3 py-1.5 text-xs font-medium rounded-md transition-colors bg-amber-100 dark:bg-amber-900/50 text-amber-700 dark:text-amber-300 hover:bg-amber-200 dark:hover:bg-amber-800/50"
379
+ >
380
+ <svg id="eye-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
381
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z"/>
382
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M2.458 12C3.732 7.943 7.523 5 12 5c4.478 0 8.268 2.943 9.542 7-1.274 4.057-5.064 7-9.542 7-4.477 0-8.268-2.943-9.542-7z"/>
383
+ </svg>
384
+ <svg id="eye-off-icon" class="w-4 h-4 hidden" fill="none" stroke="currentColor" viewBox="0 0 24 24">
385
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13.875 18.825A10.05 10.05 0 0112 19c-4.478 0-8.268-2.943-9.543-7a9.97 9.97 0 011.563-3.029m5.858.908a3 3 0 114.243 4.243M9.878 9.878l4.242 4.242M9.88 9.88l-3.29-3.29m7.532 7.532l3.29 3.29M3 3l3.59 3.59m0 0A9.953 9.953 0 0112 5c4.478 0 8.268 2.943 9.543 7a10.025 10.025 0 01-4.132 5.411m0 0L21 21"/>
386
+ </svg>
387
+ <span id="toggle-btn-text">Show Original</span>
388
+ </button>
389
+ </div>
390
+ </div>
391
+ </div>
392
+
141
393
  <!-- Parameters -->
142
394
  <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
143
395
  <div class="flex items-center justify-between mb-4">
144
396
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Parameters</h3>
145
397
  <button
146
398
  type="button"
147
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
399
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.parameters || {}))) %>"
400
+ data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.parameters || {})) %>"
148
401
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
149
402
  >
150
403
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -156,7 +409,8 @@
156
409
  <span>Copy</span>
157
410
  </button>
158
411
  </div>
159
- <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
412
+ <pre id="parameters-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.parameters || {}) %></pre>
413
+ <pre id="parameters-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.parameters || {}) %></pre>
160
414
  </div>
161
415
 
162
416
  <!-- Response -->
@@ -166,7 +420,8 @@
166
420
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Response</h3>
167
421
  <button
168
422
  type="button"
169
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
423
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.response))) %>"
424
+ data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.response)) %>"
170
425
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
171
426
  >
172
427
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -178,7 +433,8 @@
178
433
  <span>Copy</span>
179
434
  </button>
180
435
  </div>
181
- <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
436
+ <pre id="response-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json_redacted(@execution.response) %></pre>
437
+ <pre id="response-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono"><%= highlight_json(@execution.response) %></pre>
182
438
  </div>
183
439
  <% end %>
184
440
 
@@ -189,7 +445,8 @@
189
445
  <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Metadata</h3>
190
446
  <button
191
447
  type="button"
192
- data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
448
+ data-copy-json="<%= Base64.strict_encode64(JSON.pretty_generate(redact_for_display(@execution.metadata))) %>"
449
+ data-copy-json-original="<%= Base64.strict_encode64(JSON.pretty_generate(@execution.metadata)) %>"
193
450
  class="copy-json-btn inline-flex items-center gap-1.5 px-2.5 py-1.5 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded-md transition-colors"
194
451
  >
195
452
  <svg class="w-4 h-4 copy-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
@@ -201,16 +458,361 @@
201
458
  <span>Copy</span>
202
459
  </button>
203
460
  </div>
204
- <pre class="bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
461
+ <pre id="metadata-masked" class="maskable-content bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json_redacted(@execution.metadata) %></pre>
462
+ <pre id="metadata-original" class="maskable-content hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto font-mono"><%= highlight_json(@execution.metadata) %></pre>
463
+ </div>
464
+ <% end %>
465
+
466
+ <!-- Execution Hierarchy -->
467
+ <% if @execution.parent_execution_id.present? || (@execution.respond_to?(:child_executions) && @execution.child_executions.any?) %>
468
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
469
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide mb-4">Execution Hierarchy</h3>
470
+
471
+ <% if @execution.parent_execution_id.present? %>
472
+ <div class="mb-4">
473
+ <span class="text-xs text-gray-500 dark:text-gray-400">Parent Execution:</span>
474
+ <%= link_to "##{@execution.parent_execution_id}",
475
+ ruby_llm_agents.execution_path(@execution.parent_execution_id),
476
+ class: "ml-2 text-blue-600 dark:text-blue-400 hover:underline font-mono text-sm" %>
477
+ </div>
478
+ <% end %>
479
+
480
+ <% if @execution.respond_to?(:child_executions) && @execution.child_executions.any? %>
481
+ <div>
482
+ <span class="text-xs text-gray-500 dark:text-gray-400">Child Executions (<%= @execution.child_executions.count %>):</span>
483
+ <div class="mt-2 flex flex-wrap gap-2">
484
+ <% @execution.child_executions.limit(10).each do |child| %>
485
+ <%= link_to "##{child.id}",
486
+ ruby_llm_agents.execution_path(child),
487
+ class: "inline-flex items-center px-2 py-1 rounded text-xs font-mono bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600" %>
488
+ <% end %>
489
+ <% if @execution.child_executions.count > 10 %>
490
+ <span class="text-xs text-gray-400">+<%= @execution.child_executions.count - 10 %> more</span>
491
+ <% end %>
492
+ </div>
493
+ </div>
494
+ <% end %>
495
+ </div>
496
+ <% end %>
497
+
498
+ <!-- System Prompt -->
499
+ <% if @execution.system_prompt.present? %>
500
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
501
+ <div class="flex items-center justify-between mb-4">
502
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">System Prompt</h3>
503
+ <button type="button" onclick="togglePrompt('system')" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
504
+ <span id="system-prompt-toggle">Expand</span>
505
+ </button>
506
+ </div>
507
+ <pre id="system-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.system_prompt %></pre>
205
508
  </div>
206
509
  <% end %>
207
510
 
511
+ <!-- User Prompt -->
512
+ <% if @execution.user_prompt.present? %>
513
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
514
+ <div class="flex items-center justify-between mb-4">
515
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">User Prompt</h3>
516
+ <button type="button" onclick="togglePrompt('user')" class="text-xs text-gray-500 hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
517
+ <span id="user-prompt-toggle">Expand</span>
518
+ </button>
519
+ </div>
520
+ <pre id="user-prompt-content" class="hidden bg-gray-50 dark:bg-gray-900 text-gray-900 dark:text-gray-100 rounded-lg p-4 text-sm overflow-x-auto max-h-96 font-mono whitespace-pre-wrap"><%= @execution.user_prompt %></pre>
521
+ </div>
522
+ <% end %>
523
+
524
+ <!-- Diagnostics Panel -->
525
+ <div class="bg-white dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded-xl p-6 mb-6">
526
+ <div class="flex items-center justify-between mb-4">
527
+ <div class="flex items-center gap-2">
528
+ <svg class="w-5 h-5 text-gray-500 dark:text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
529
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"/>
530
+ </svg>
531
+ <h3 class="text-sm font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wide">Diagnostics</h3>
532
+ </div>
533
+ <button
534
+ type="button"
535
+ id="toggle-diagnostics-btn"
536
+ onclick="toggleDiagnostics()"
537
+ class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
538
+ >
539
+ <svg id="diagnostics-expand-icon" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
540
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7"/>
541
+ </svg>
542
+ <span id="diagnostics-toggle-text">Expand</span>
543
+ </button>
544
+ </div>
545
+
546
+ <!-- Quick Info (always visible) -->
547
+ <div class="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
548
+ <div>
549
+ <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Model</span>
550
+ <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.model_id %></p>
551
+ </div>
552
+ <div>
553
+ <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Temperature</span>
554
+ <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.temperature || 'N/A' %></p>
555
+ </div>
556
+ <div>
557
+ <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Version</span>
558
+ <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.agent_version || '1.0' %></p>
559
+ </div>
560
+ <div>
561
+ <span class="text-gray-500 dark:text-gray-400 text-xs uppercase tracking-wide">Status</span>
562
+ <p class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.status %></p>
563
+ </div>
564
+ </div>
565
+
566
+ <!-- Expanded Details (hidden by default) -->
567
+ <div id="diagnostics-details" class="hidden mt-6 pt-6 border-t border-gray-100 dark:border-gray-700">
568
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6">
569
+ <!-- Timing Information -->
570
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
571
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Timing</h4>
572
+ <dl class="space-y-2 text-sm">
573
+ <div class="flex justify-between">
574
+ <dt class="text-gray-500 dark:text-gray-400">Started</dt>
575
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.started_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
576
+ </div>
577
+ <div class="flex justify-between">
578
+ <dt class="text-gray-500 dark:text-gray-400">Completed</dt>
579
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.completed_at&.strftime("%Y-%m-%d %H:%M:%S.%L") || 'N/A' %></dd>
580
+ </div>
581
+ <div class="flex justify-between">
582
+ <dt class="text-gray-500 dark:text-gray-400">Duration</dt>
583
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.duration_ms ? "#{@execution.duration_ms}ms" : 'N/A' %></dd>
584
+ </div>
585
+ <% if @execution.streaming? && @execution.time_to_first_token_ms %>
586
+ <div class="flex justify-between">
587
+ <dt class="text-gray-500 dark:text-gray-400">Time to First Token</dt>
588
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.time_to_first_token_ms %>ms</dd>
589
+ </div>
590
+ <% end %>
591
+ <div class="flex justify-between">
592
+ <dt class="text-gray-500 dark:text-gray-400">Created At</dt>
593
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.created_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
594
+ </div>
595
+ </dl>
596
+ </div>
597
+
598
+ <!-- Performance Metrics -->
599
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
600
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Performance</h4>
601
+ <dl class="space-y-2 text-sm">
602
+ <div class="flex justify-between">
603
+ <dt class="text-gray-500 dark:text-gray-400">Tokens/Second</dt>
604
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.tokens_per_second&.round(1) || 'N/A' %></dd>
605
+ </div>
606
+ <div class="flex justify-between">
607
+ <dt class="text-gray-500 dark:text-gray-400">Input Tokens</dt>
608
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.input_tokens || 0 %></dd>
609
+ </div>
610
+ <div class="flex justify-between">
611
+ <dt class="text-gray-500 dark:text-gray-400">Output Tokens</dt>
612
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.output_tokens || 0 %></dd>
613
+ </div>
614
+ <div class="flex justify-between">
615
+ <dt class="text-gray-500 dark:text-gray-400">Cached Tokens</dt>
616
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_tokens || 0 %></dd>
617
+ </div>
618
+ </dl>
619
+ </div>
620
+
621
+ <!-- Cost Breakdown -->
622
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
623
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Cost</h4>
624
+ <dl class="space-y-2 text-sm">
625
+ <div class="flex justify-between">
626
+ <dt class="text-gray-500 dark:text-gray-400">Input Cost</dt>
627
+ <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.input_cost || 0, precision: 6) %></dd>
628
+ </div>
629
+ <div class="flex justify-between">
630
+ <dt class="text-gray-500 dark:text-gray-400">Output Cost</dt>
631
+ <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.output_cost || 0, precision: 6) %></dd>
632
+ </div>
633
+ <div class="flex justify-between font-semibold">
634
+ <dt class="text-gray-600 dark:text-gray-300">Total Cost</dt>
635
+ <dd class="font-mono text-gray-900 dark:text-gray-100">$<%= number_with_precision(@execution.total_cost || 0, precision: 6) %></dd>
636
+ </div>
637
+ </dl>
638
+ </div>
639
+
640
+ <!-- Configuration -->
641
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
642
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Configuration</h4>
643
+ <dl class="space-y-2 text-sm">
644
+ <div class="flex justify-between">
645
+ <dt class="text-gray-500 dark:text-gray-400">Agent Type</dt>
646
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.agent_type %></dd>
647
+ </div>
648
+ <div class="flex justify-between">
649
+ <dt class="text-gray-500 dark:text-gray-400">Temperature</dt>
650
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.temperature || 'default' %></dd>
651
+ </div>
652
+ <% if @execution.respond_to?(:chosen_model_id) && @execution.chosen_model_id.present? && @execution.chosen_model_id != @execution.model_id %>
653
+ <div class="flex justify-between">
654
+ <dt class="text-gray-500 dark:text-gray-400">Chosen Model</dt>
655
+ <dd class="font-mono text-amber-600 dark:text-amber-400"><%= @execution.chosen_model_id %></dd>
656
+ </div>
657
+ <% end %>
658
+ <% if @execution.respond_to?(:attempts_count) && @execution.attempts_count && @execution.attempts_count > 1 %>
659
+ <div class="flex justify-between">
660
+ <dt class="text-gray-500 dark:text-gray-400">Retry Count</dt>
661
+ <dd class="font-mono text-blue-600 dark:text-blue-400"><%= @execution.attempts_count - 1 %></dd>
662
+ </div>
663
+ <% end %>
664
+ </dl>
665
+ </div>
666
+ </div>
667
+
668
+ <!-- Second Row: Tracing and Caching -->
669
+ <div class="grid grid-cols-1 md:grid-cols-2 gap-6 mt-6">
670
+ <!-- Tracing Information -->
671
+ <% if @execution.trace_id.present? || @execution.request_id.present? %>
672
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
673
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Tracing</h4>
674
+ <dl class="space-y-2 text-sm">
675
+ <% if @execution.request_id.present? %>
676
+ <div class="flex justify-between">
677
+ <dt class="text-gray-500 dark:text-gray-400">Request ID</dt>
678
+ <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.request_id %></dd>
679
+ </div>
680
+ <% end %>
681
+ <% if @execution.trace_id.present? %>
682
+ <div class="flex justify-between">
683
+ <dt class="text-gray-500 dark:text-gray-400">Trace ID</dt>
684
+ <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.trace_id %></dd>
685
+ </div>
686
+ <% end %>
687
+ <% if @execution.span_id.present? %>
688
+ <div class="flex justify-between">
689
+ <dt class="text-gray-500 dark:text-gray-400">Span ID</dt>
690
+ <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs"><%= @execution.span_id %></dd>
691
+ </div>
692
+ <% end %>
693
+ </dl>
694
+ </div>
695
+ <% end %>
696
+
697
+ <!-- Caching Information -->
698
+ <% if @execution.cache_hit || @execution.response_cache_key.present? %>
699
+ <div class="bg-gray-50 dark:bg-gray-900/50 rounded-lg p-4">
700
+ <h4 class="text-xs font-semibold text-gray-700 dark:text-gray-300 uppercase tracking-wide mb-3">Caching</h4>
701
+ <dl class="space-y-2 text-sm">
702
+ <div class="flex justify-between">
703
+ <dt class="text-gray-500 dark:text-gray-400">Cache Hit</dt>
704
+ <dd class="font-mono text-gray-900 dark:text-gray-100">
705
+ <% if @execution.cache_hit %>
706
+ <span class="text-green-600 dark:text-green-400">Yes</span>
707
+ <% else %>
708
+ <span class="text-gray-400">No</span>
709
+ <% end %>
710
+ </dd>
711
+ </div>
712
+ <% if @execution.response_cache_key.present? %>
713
+ <div class="flex justify-between">
714
+ <dt class="text-gray-500 dark:text-gray-400">Cache Key</dt>
715
+ <dd class="font-mono text-gray-900 dark:text-gray-100 text-xs truncate max-w-xs" title="<%= @execution.response_cache_key %>">
716
+ <%= @execution.response_cache_key.truncate(30) %>
717
+ </dd>
718
+ </div>
719
+ <% end %>
720
+ <% if @execution.cached_at.present? %>
721
+ <div class="flex justify-between">
722
+ <dt class="text-gray-500 dark:text-gray-400">Cached At</dt>
723
+ <dd class="font-mono text-gray-900 dark:text-gray-100"><%= @execution.cached_at.strftime("%Y-%m-%d %H:%M:%S") %></dd>
724
+ </div>
725
+ <% end %>
726
+ </dl>
727
+ </div>
728
+ <% end %>
729
+ </div>
730
+
731
+ <!-- Execution ID -->
732
+ <div class="mt-4 pt-4 border-t border-gray-100 dark:border-gray-700">
733
+ <div class="flex items-center justify-between">
734
+ <span class="text-xs text-gray-500 dark:text-gray-400">
735
+ Execution ID: <code class="bg-gray-100 dark:bg-gray-700 px-1.5 py-0.5 rounded font-mono"><%= @execution.id %></code>
736
+ </span>
737
+ <button
738
+ type="button"
739
+ onclick="copyDiagnostics()"
740
+ class="inline-flex items-center gap-1 px-2 py-1 text-xs font-medium text-gray-500 dark:text-gray-400 hover:text-gray-700 dark:hover:text-gray-200 hover:bg-gray-100 dark:hover:bg-gray-700 rounded transition-colors"
741
+ >
742
+ <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
743
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z"/>
744
+ </svg>
745
+ Copy All
746
+ </button>
747
+ </div>
748
+ </div>
749
+ </div>
750
+ </div>
751
+
208
752
  <script>
753
+ // Prompt toggle function
754
+ function togglePrompt(type) {
755
+ const content = document.getElementById(type + '-prompt-content');
756
+ const toggle = document.getElementById(type + '-prompt-toggle');
757
+ content.classList.toggle('hidden');
758
+ toggle.textContent = content.classList.contains('hidden') ? 'Expand' : 'Collapse';
759
+ }
760
+
761
+ // Masking state (persisted in localStorage)
762
+ let isMasked = localStorage.getItem('ruby_llm_agents_masking') !== 'false';
763
+
764
+ function toggleMasking() {
765
+ isMasked = !isMasked;
766
+ localStorage.setItem('ruby_llm_agents_masking', isMasked);
767
+ updateMaskingUI();
768
+ }
769
+
770
+ function updateMaskingUI() {
771
+ const status = document.getElementById('masking-status');
772
+ const btnText = document.getElementById('toggle-btn-text');
773
+ const eyeIcon = document.getElementById('eye-icon');
774
+ const eyeOffIcon = document.getElementById('eye-off-icon');
775
+
776
+ // Update all maskable content sections
777
+ document.querySelectorAll('[id$="-masked"]').forEach(function(el) {
778
+ el.classList.toggle('hidden', !isMasked);
779
+ });
780
+ document.querySelectorAll('[id$="-original"]').forEach(function(el) {
781
+ el.classList.toggle('hidden', isMasked);
782
+ });
783
+
784
+ // Update status and button
785
+ if (isMasked) {
786
+ status.textContent = 'Sensitive data is hidden';
787
+ btnText.textContent = 'Show Original';
788
+ eyeIcon.classList.remove('hidden');
789
+ eyeOffIcon.classList.add('hidden');
790
+ } else {
791
+ status.textContent = 'Showing original data';
792
+ btnText.textContent = 'Hide Sensitive';
793
+ eyeIcon.classList.add('hidden');
794
+ eyeOffIcon.classList.remove('hidden');
795
+ }
796
+
797
+ // Update copy buttons to use appropriate data
798
+ document.querySelectorAll('.copy-json-btn').forEach(function(button) {
799
+ const maskedData = button.getAttribute('data-copy-json');
800
+ const originalData = button.getAttribute('data-copy-json-original');
801
+ if (originalData) {
802
+ button.setAttribute('data-active-json', isMasked ? maskedData : originalData);
803
+ }
804
+ });
805
+ }
806
+
209
807
  document.addEventListener('DOMContentLoaded', function() {
808
+ // Initialize masking UI on page load
809
+ updateMaskingUI();
810
+
210
811
  document.querySelectorAll('.copy-json-btn').forEach(function(button) {
211
812
  button.addEventListener('click', function() {
212
- const base64Data = this.getAttribute('data-copy-json');
213
- const jsonText = atob(base64Data);
813
+ // Use active JSON (considers masking state) or fall back to default
814
+ const activeData = this.getAttribute('data-active-json') || this.getAttribute('data-copy-json');
815
+ const jsonText = atob(activeData);
214
816
  const span = this.querySelector('span');
215
817
  const copyIcon = this.querySelector('.copy-icon');
216
818
  const checkIcon = this.querySelector('.check-icon');
@@ -237,4 +839,81 @@
237
839
  });
238
840
  });
239
841
  });
842
+
843
+ // Rerun modal functions
844
+ function confirmRerun() {
845
+ document.getElementById('rerun-modal').classList.remove('hidden');
846
+ document.body.classList.add('overflow-hidden');
847
+ }
848
+
849
+ function closeRerunModal() {
850
+ document.getElementById('rerun-modal').classList.add('hidden');
851
+ document.body.classList.remove('overflow-hidden');
852
+ }
853
+
854
+ // Close modal on Escape key
855
+ document.addEventListener('keydown', function(e) {
856
+ if (e.key === 'Escape') {
857
+ closeRerunModal();
858
+ }
859
+ });
860
+
861
+ // Diagnostics panel toggle
862
+ let diagnosticsExpanded = localStorage.getItem('ruby_llm_agents_diagnostics_expanded') === 'true';
863
+
864
+ function toggleDiagnostics() {
865
+ diagnosticsExpanded = !diagnosticsExpanded;
866
+ localStorage.setItem('ruby_llm_agents_diagnostics_expanded', diagnosticsExpanded);
867
+ updateDiagnosticsUI();
868
+ }
869
+
870
+ function updateDiagnosticsUI() {
871
+ const details = document.getElementById('diagnostics-details');
872
+ const toggleText = document.getElementById('diagnostics-toggle-text');
873
+ const expandIcon = document.getElementById('diagnostics-expand-icon');
874
+
875
+ if (diagnosticsExpanded) {
876
+ details.classList.remove('hidden');
877
+ toggleText.textContent = 'Collapse';
878
+ expandIcon.style.transform = 'rotate(180deg)';
879
+ } else {
880
+ details.classList.add('hidden');
881
+ toggleText.textContent = 'Expand';
882
+ expandIcon.style.transform = 'rotate(0deg)';
883
+ }
884
+ }
885
+
886
+ function copyDiagnostics() {
887
+ const diagnostics = {
888
+ execution_id: <%= @execution.id %>,
889
+ agent_type: "<%= @execution.agent_type %>",
890
+ agent_version: "<%= @execution.agent_version || '1.0' %>",
891
+ model_id: "<%= @execution.model_id %>",
892
+ status: "<%= @execution.status %>",
893
+ temperature: <%= @execution.temperature || 'null' %>,
894
+ duration_ms: <%= @execution.duration_ms || 'null' %>,
895
+ input_tokens: <%= @execution.input_tokens || 0 %>,
896
+ output_tokens: <%= @execution.output_tokens || 0 %>,
897
+ cached_tokens: <%= @execution.cached_tokens || 0 %>,
898
+ total_tokens: <%= @execution.total_tokens || 0 %>,
899
+ input_cost: <%= @execution.input_cost || 0 %>,
900
+ output_cost: <%= @execution.output_cost || 0 %>,
901
+ total_cost: <%= @execution.total_cost || 0 %>,
902
+ started_at: "<%= @execution.started_at&.iso8601 || '' %>",
903
+ completed_at: "<%= @execution.completed_at&.iso8601 || '' %>",
904
+ created_at: "<%= @execution.created_at.iso8601 %>"
905
+ };
906
+
907
+ navigator.clipboard.writeText(JSON.stringify(diagnostics, null, 2)).then(function() {
908
+ alert('Diagnostics copied to clipboard!');
909
+ }).catch(function(err) {
910
+ console.error('Failed to copy diagnostics:', err);
911
+ });
912
+ }
913
+
914
+ // Initialize diagnostics panel on page load
915
+ document.addEventListener('DOMContentLoaded', function() {
916
+ updateDiagnosticsUI();
917
+ });
240
918
  </script>
919
+ </div>