leva 0.1.10 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -2
  3. data/app/assets/stylesheets/leva/application.css +3083 -15
  4. data/app/controllers/leva/design_system_controller.rb +9 -0
  5. data/app/views/layouts/leva/application.html.erb +23 -24
  6. data/app/views/leva/dataset_records/index.html.erb +63 -61
  7. data/app/views/leva/dataset_records/show.html.erb +115 -25
  8. data/app/views/leva/datasets/_dataset.html.erb +11 -18
  9. data/app/views/leva/datasets/_form.html.erb +18 -14
  10. data/app/views/leva/datasets/edit.html.erb +16 -4
  11. data/app/views/leva/datasets/index.html.erb +33 -41
  12. data/app/views/leva/datasets/new.html.erb +15 -4
  13. data/app/views/leva/datasets/show.html.erb +120 -139
  14. data/app/views/leva/design_system/index.html.erb +1731 -0
  15. data/app/views/leva/experiments/_experiment.html.erb +46 -31
  16. data/app/views/leva/experiments/_form.html.erb +62 -35
  17. data/app/views/leva/experiments/edit.html.erb +17 -3
  18. data/app/views/leva/experiments/index.html.erb +41 -36
  19. data/app/views/leva/experiments/new.html.erb +40 -19
  20. data/app/views/leva/experiments/show.html.erb +155 -98
  21. data/app/views/leva/runner_results/show.html.erb +271 -54
  22. data/app/views/leva/workbench/_evaluation_area.html.erb +18 -4
  23. data/app/views/leva/workbench/_prompt_content.html.erb +116 -111
  24. data/app/views/leva/workbench/_prompt_form.html.erb +24 -23
  25. data/app/views/leva/workbench/_prompt_sidebar.html.erb +57 -12
  26. data/app/views/leva/workbench/_results_section.html.erb +274 -112
  27. data/app/views/leva/workbench/_top_bar.html.erb +16 -6
  28. data/app/views/leva/workbench/edit.html.erb +46 -15
  29. data/app/views/leva/workbench/index.html.erb +5 -8
  30. data/app/views/leva/workbench/new.html.erb +74 -42
  31. data/config/routes.rb +2 -0
  32. data/lib/leva/engine.rb +10 -0
  33. data/lib/leva/version.rb +1 -1
  34. metadata +4 -2
@@ -1,126 +1,182 @@
1
- <div class="w-1/2 bg-gray-900 border-l border-gray-800 p-5 overflow-y-auto overflow-x-hidden" data-controller="button-loader">
2
- <!-- Runner Dropdown -->
3
- <div class="mb-5">
4
- <h3 class="text-sm font-semibold mb-2 text-indigo-300">Select Runner</h3>
5
- <%= form_tag workbench_index_path, method: :get, class: "flex items-center" do %>
6
- <%= select_tag :runner,
7
- options_from_collection_for_select(@runners, :name, :name, @selected_runner),
8
- include_blank: "Select a runner",
9
- onchange: "this.form.submit()",
10
- class: "w-full bg-gray-800 text-white border border-gray-700 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" %>
11
- <%= hidden_field_tag :prompt_id, @prompt&.id %>
12
- <%= hidden_field_tag :dataset_record_id, @selected_dataset_record %>
13
- <% end %>
14
- </div>
15
- <!-- Dataset Record Dropdown -->
16
- <div class="mb-5">
17
- <%= form_tag workbench_index_path, method: :get, class: "flex flex-col" do %>
18
- <%= label_tag :dataset_record_id, "Select Test Record", class: "text-sm font-semibold mb-2 text-indigo-300" %>
19
- <%= select_tag :dataset_record_id,
20
- options_from_collection_for_select(Leva::DatasetRecord.all, :id, :display_name, @selected_dataset_record),
21
- include_blank: "Select a record",
22
- onchange: "this.form.submit()",
23
- class: "w-full bg-gray-800 text-white border border-gray-700 rounded px-3 py-2 focus:outline-none focus:ring-2 focus:ring-indigo-500" %>
1
+ <div class="panel-right" data-controller="button-loader collapsible-panel" data-resizable-target="panel" data-collapsible-panel-key-value="leva-panel-right-collapsed" style="position: relative;">
2
+ <button type="button" class="panel-right-collapse-btn" data-action="click->collapsible-panel#toggle" title="Toggle panel">
3
+ <svg viewBox="0 0 20 20" fill="currentColor">
4
+ <path fill-rule="evenodd" d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z" clip-rule="evenodd" />
5
+ </svg>
6
+ </button>
7
+ <%# Run Controls %>
8
+ <div class="run-controls">
9
+ <div class="flex items-center gap-2 mb-3">
10
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
11
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10.325 4.317c.426-1.756 2.924-1.756 3.35 0a1.724 1.724 0 002.573 1.066c1.543-.94 3.31.826 2.37 2.37a1.724 1.724 0 001.065 2.572c1.756.426 1.756 2.924 0 3.35a1.724 1.724 0 00-1.066 2.573c.94 1.543-.826 3.31-2.37 2.37a1.724 1.724 0 00-2.572 1.065c-.426 1.756-2.924 1.756-3.35 0a1.724 1.724 0 00-2.573-1.066c-1.543.94-3.31-.826-2.37-2.37a1.724 1.724 0 00-1.065-2.572c-1.756-.426-1.756-2.924 0-3.35a1.724 1.724 0 001.066-2.573c-.94-1.543.826-3.31 2.37-2.37.996.608 2.296.07 2.572-1.065z" />
12
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
13
+ </svg>
14
+ <span class="text-xs font-semibold uppercase text-muted" style="letter-spacing: 0.05em;">Run Configuration</span>
15
+ </div>
16
+ <%= form_tag workbench_index_path, method: :get, class: "run-form" do %>
17
+ <div class="run-selects">
18
+ <div class="run-select-group">
19
+ <label class="run-label">Runner</label>
20
+ <%= select_tag :runner,
21
+ options_from_collection_for_select(@runners, :name, :name, @selected_runner),
22
+ include_blank: "Select runner...",
23
+ onchange: "this.form.submit()",
24
+ class: "form-select form-select-sm" %>
25
+ </div>
26
+ <div class="run-select-group">
27
+ <label class="run-label">Record</label>
28
+ <%= select_tag :dataset_record_id,
29
+ options_from_collection_for_select(Leva::DatasetRecord.all, :id, :display_name, @selected_dataset_record),
30
+ include_blank: "Select record...",
31
+ onchange: "this.form.submit()",
32
+ class: "form-select form-select-sm" %>
33
+ </div>
34
+ </div>
24
35
  <%= hidden_field_tag :prompt_id, @prompt&.id %>
25
- <%= hidden_field_tag :runner, @selected_runner %>
26
36
  <% end %>
27
- </div>
28
- <!-- Run Button -->
29
- <div class="mb-5">
30
- <%= button_to run_workbench_index_path, method: :post, params: { runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "w-full btn btn-primary bg-gradient-to-r from-blue-500 to-indigo-600 hover:from-blue-600 hover:to-indigo-700 text-white font-bold py-2 px-4 rounded-lg shadow-lg flex items-center justify-center transition duration-300 ease-in-out h-12", data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
31
- <span data-button-loader-target="buttonText" class="flex items-center">
32
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
37
+
38
+ <%= button_to run_workbench_index_path, method: :post, params: { runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "btn btn-primary btn-run", disabled: @selected_runner.blank? || @selected_dataset_record.blank?, data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
39
+ <span data-button-loader-target="buttonText" class="flex items-center justify-center">
40
+ <svg class="icon-sm" style="margin-right: 6px;" viewBox="0 0 20 20" fill="currentColor">
33
41
  <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
34
42
  </svg>
35
- Run
43
+ Run Prompt
36
44
  </span>
37
45
  <% end %>
38
46
  </div>
39
- <!-- Run Result -->
40
- <div class="bg-gray-800 p-4 rounded-lg mb-5 shadow-md">
41
- <h3 class="text-sm font-semibold mb-2 text-indigo-300">Run Result</h3>
42
- <% if @dataset_record && (runner_result = @dataset_record.runner_results.last) %>
43
- <div class="mb-3">
44
- <h4 class="text-xs font-semibold text-indigo-200 mb-1">Ground Truth:</h4>
45
- <pre class="text-sm text-gray-300 whitespace-pre-wrap break-words overflow-x-auto max-w-full bg-gray-700 p-2 rounded"><%= runner_result.ground_truth %></pre>
46
- </div>
47
- <div>
48
- <h4 class="text-xs font-semibold text-indigo-200 mb-1">Raw Prediction:</h4>
49
- <pre class="text-sm text-gray-300 whitespace-pre-wrap break-words overflow-x-auto max-w-full bg-gray-700 p-2 rounded"><%= runner_result.prediction %></pre>
47
+
48
+ <%# Output Section %>
49
+ <div class="output-section">
50
+ <div class="output-header">
51
+ <div class="flex items-center gap-2">
52
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
53
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
54
+ </svg>
55
+ <span class="output-title">Output</span>
50
56
  </div>
51
- <% if runner_result.dataset_record.recordable.extract_regex_pattern %>
52
- <div>
53
- <h4 class="text-xs font-semibold text-indigo-200 my-2 gap-2">Parsed Predictions: <%= runner_result.dataset_record.recordable.extract_regex_pattern.to_s %></h4>
54
- <% runner_result.parsed_predictions.each do |prediction| %>
55
- <pre class="text-sm text-gray-300 whitespace-pre-wrap break-words overflow-x-auto max-w-full bg-gray-700 p-2 rounded mb-2"><%= prediction %></pre>
56
- <% end %>
57
- </div>
57
+ <% if @dataset_record && (runner_result = @dataset_record.runner_results.last) %>
58
+ <span class="output-meta">
59
+ <span class="badge badge-default" style="font-size: 10px; padding: 2px 6px;">v<%= runner_result.prompt.version %></span>
60
+ <span style="margin-left: 4px;"><%= time_ago_in_words(runner_result.created_at) %> ago</span>
61
+ </span>
58
62
  <% end %>
59
- <div class="flex justify-between items-center mt-2 text-xs text-gray-500">
60
- <p>Prompt version: <%= runner_result.prompt.version %></p>
61
- <p>Run <%= time_ago_in_words(runner_result.created_at) %> ago</p>
63
+ </div>
64
+
65
+ <% if @dataset_record && (runner_result = @dataset_record.runner_results.last) %>
66
+ <div class="output-grid">
67
+ <div class="output-block output-block--expected">
68
+ <span class="output-label">
69
+ <svg class="icon-sm" style="display: inline; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; color: var(--success-500);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
70
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
71
+ </svg>
72
+ Expected
73
+ </span>
74
+ <span class="output-value"><%= runner_result.ground_truth %></span>
75
+ </div>
76
+ <div class="output-block output-block--got">
77
+ <span class="output-label">
78
+ <svg class="icon-sm" style="display: inline; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; color: var(--gray-500);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
79
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
80
+ </svg>
81
+ Result
82
+ </span>
83
+ <span class="output-value"><%= runner_result.prediction %></span>
84
+ </div>
85
+ <% if runner_result.dataset_record.recordable.extract_regex_pattern && runner_result.parsed_predictions.any? %>
86
+ <div class="output-block" style="background: rgba(106, 159, 196, 0.08); border-left: 2px solid var(--info-500);">
87
+ <span class="output-label">
88
+ <svg class="icon-sm" style="display: inline; width: 12px; height: 12px; margin-right: 4px; vertical-align: -2px; color: var(--info-500);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
89
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21a4 4 0 01-4-4V5a2 2 0 012-2h4a2 2 0 012 2v12a4 4 0 01-4 4zm0 0h12a2 2 0 002-2v-4a2 2 0 00-2-2h-2.343M11 7.343l1.657-1.657a2 2 0 012.828 0l2.829 2.829a2 2 0 010 2.828l-8.486 8.485M7 17h.01" />
90
+ </svg>
91
+ Parsed
92
+ </span>
93
+ <span class="output-value"><%= runner_result.parsed_predictions.join(", ") %></span>
94
+ </div>
95
+ <% end %>
62
96
  </div>
63
97
  <% else %>
64
- <div class="text-center py-8">
65
- <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto text-gray-500 mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
66
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
98
+ <div class="output-empty">
99
+ <svg class="icon-lg" style="margin: 0 auto var(--space-2); color: var(--gray-600);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
100
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
101
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
67
102
  </svg>
68
- <p class="text-sm text-gray-500">No results yet. Click 'Run' to start the analysis.</p>
103
+ <p class="text-sm text-muted">Click Run to see output</p>
69
104
  </div>
70
105
  <% end %>
71
106
  </div>
72
- <!-- Evaluators -->
73
- <div class="space-y-4">
74
- <div class="flex items-center justify-between">
75
- <h3 class="text-sm font-semibold text-indigo-400">Evaluators</h3>
76
- <%= button_to run_all_evals_workbench_index_path, method: :post, params: { runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "btn btn-primary bg-gradient-to-r from-green-500 to-teal-600 hover:from-green-600 hover:to-teal-700 text-white font-bold py-2 px-4 rounded-lg shadow-lg flex items-center justify-center transition duration-300 ease-in-out h-10", data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
77
- <span data-button-loader-target="buttonText" class="flex items-center">
78
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5 mr-2" viewBox="0 0 20 20" fill="currentColor">
79
- <path fill-rule="evenodd" d="M6 2a2 2 0 00-2 2v12a2 2 0 002 2h8a2 2 0 002-2V7.414A2 2 0 0015.414 6L12 2.586A2 2 0 0010.586 2H6zm2 10a1 1 0 10-2 0v3a1 1 0 102 0v-3zm2-3a1 1 0 011 1v5a1 1 0 11-2 0v-5a1 1 0 011-1zm4-1a1 1 0 10-2 0v7a1 1 0 102 0V8z" clip-rule="evenodd" />
80
- </svg>
81
- Evaluate All
82
- </span>
107
+
108
+ <%# Evaluators Section %>
109
+ <div class="eval-section">
110
+ <div class="eval-header">
111
+ <div class="flex items-center gap-2">
112
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
113
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
114
+ </svg>
115
+ <span class="eval-title">Evaluations</span>
116
+ </div>
117
+ <% if @evaluators.any? %>
118
+ <%= button_to run_all_evals_workbench_index_path, method: :post, params: { runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "btn btn-ghost btn-sm", disabled: @selected_dataset_record.blank?, style: "font-size: 11px;", data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
119
+ <span data-button-loader-target="buttonText" class="flex items-center gap-1">
120
+ <svg class="icon-sm" style="width: 12px; height: 12px;" fill="none" viewBox="0 0 24 24" stroke="currentColor">
121
+ <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" />
122
+ </svg>
123
+ Run All
124
+ </span>
125
+ <% end %>
83
126
  <% end %>
84
127
  </div>
85
- <div class="grid grid-cols-2 gap-2">
86
- <% @evaluators.each do |evaluator_class| %>
87
- <div class="bg-gray-800 p-4 rounded-lg shadow-md">
88
- <div class="flex items-center justify-between mb-2">
89
- <span class="text-sm font-medium"><%= evaluator_class.name.demodulize %></span>
90
- <%= button_to run_evaluator_workbench_index_path, method: :post, params: { evaluator: evaluator_class.name, runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "p-2 bg-blue-600 hover:bg-blue-700 rounded-full transition duration-150 ease-in-out", data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
91
- <span data-button-loader-target="buttonText">
92
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
93
- <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM9.555 7.168A1 1 0 008 8v4a1 1 0 001.555.832l3-2a1 1 0 000-1.664l-3-2z" clip-rule="evenodd" />
94
- </svg>
95
- </span>
96
- <% end %>
97
- </div>
98
- <% if @dataset_record %>
99
- <% evaluation_result = @dataset_record.evaluation_results.for_evaluator(evaluator_class).last %>
100
- <% if evaluation_result %>
101
- <% score = evaluation_result.score %>
102
- <% color_class = case score
103
- when 0...0.2 then 'text-red-500'
104
- when 0.2...0.4 then 'text-orange-500'
105
- when 0.4...0.6 then 'text-yellow-500'
106
- when 0.6...0.8 then 'text-lime-500'
107
- when 0.8...1.0 then 'text-green-400'
108
- else 'text-green-300'
109
- end %>
110
- <div class="text-sm <%= color_class %> font-semibold">
111
- Score: <%= sprintf('%.2f', score) %>
128
+ <% if @evaluators.any? %>
129
+ <div class="eval-grid">
130
+ <% @evaluators.each do |evaluator_class| %>
131
+ <% evaluation_result = @dataset_record&.evaluation_results&.for_evaluator(evaluator_class)&.last %>
132
+ <% score = evaluation_result&.score %>
133
+ <%
134
+ score_class = case score
135
+ when 0...0.2 then 'score-bad'
136
+ when 0.2...0.4 then 'score-poor'
137
+ when 0.4...0.6 then 'score-fair'
138
+ when 0.6...0.8 then 'score-good'
139
+ when 0.8..1.0 then 'score-excellent'
140
+ else ''
141
+ end
142
+ bg_style = case score
143
+ when 0...0.2 then 'background: rgba(207, 111, 98, 0.08);'
144
+ when 0.2...0.4 then 'background: rgba(232, 161, 88, 0.08);'
145
+ when 0.4...0.6 then 'background: rgba(212, 168, 74, 0.08);'
146
+ when 0.6...0.8 then 'background: rgba(159, 204, 111, 0.08);'
147
+ when 0.8..1.0 then 'background: rgba(125, 179, 103, 0.08);'
148
+ else ''
149
+ end
150
+ %>
151
+ <%= button_to run_evaluator_workbench_index_path, method: :post, params: { evaluator: evaluator_class.name, runner: @selected_runner, prompt_id: @prompt&.id, dataset_record_id: @selected_dataset_record }, class: "eval-card #{score ? 'eval-card--has-score' : ''}", style: bg_style, disabled: @selected_dataset_record.blank?, data: { action: "click->button-loader#handleClick", "button-loader-target": "button" } do %>
152
+ <span data-button-loader-target="buttonText" class="eval-card-inner">
153
+ <span class="eval-name"><%= evaluator_class.name.demodulize.gsub(/Evaluator$/, '').gsub(/([a-z])([A-Z])/, '\1 \2') %></span>
154
+ <div class="flex items-center gap-2">
155
+ <% if score %>
156
+ <span class="eval-score <%= score_class %>"><%= sprintf('%.0f', score * 100) %></span>
157
+ <span class="text-xs text-muted" style="font-size: 10px;">%</span>
158
+ <% else %>
159
+ <span class="eval-score eval-score--empty">
160
+ <svg class="icon-sm" style="width: 14px; height: 14px; color: var(--gray-600);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
161
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
162
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
163
+ </svg>
164
+ </span>
165
+ <% end %>
112
166
  </div>
113
- <% else %>
114
- <div class="text-sm text-gray-500">
115
- No evaluation result yet.
116
- </div>
117
- <% end %>
167
+ </span>
118
168
  <% end %>
119
- </div>
120
- <% end %>
121
- </div>
169
+ <% end %>
170
+ </div>
171
+ <% else %>
172
+ <div class="text-center p-4" style="background: var(--gray-850, #222120); border-radius: var(--radius-md);">
173
+ <p class="text-sm text-muted">No evaluators configured</p>
174
+ <p class="text-xs text-subtle mt-1">Add evaluators in app/evals</p>
175
+ </div>
176
+ <% end %>
122
177
  </div>
123
178
  </div>
179
+
124
180
  <script>
125
181
  (() => {
126
182
  const application = Stimulus.Application.start()
@@ -135,25 +191,131 @@
135
191
 
136
192
  this.disableButton(button)
137
193
  this.showSpinner(button)
138
-
139
- // Submit the form
140
194
  form.submit()
141
195
  }
142
196
 
143
197
  disableButton(button) {
144
198
  button.disabled = true
145
- button.classList.add('opacity-50', 'cursor-not-allowed')
199
+ button.style.opacity = '0.5'
200
+ button.style.cursor = 'not-allowed'
146
201
  }
147
202
 
148
203
  showSpinner(button) {
149
204
  const buttonText = button.querySelector('[data-button-loader-target="buttonText"]')
150
- buttonText.innerHTML = `
151
- <svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
152
- <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
153
- <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
154
- </svg>
155
- `
205
+ buttonText.innerHTML = '<span class="spinner"></span>'
206
+ }
207
+ })
208
+
209
+ application.register("resizable", class extends Stimulus.Controller {
210
+ static targets = ["handle", "panel"]
211
+
212
+ connect() {
213
+ this.isResizing = false
214
+ this.startX = 0
215
+ this.startWidth = 0
216
+ this.minWidth = 200
217
+ this.maxWidth = window.innerWidth * 0.5
218
+
219
+ // Restore saved width from localStorage
220
+ const savedWidth = localStorage.getItem('leva-panel-right-width')
221
+ if (savedWidth && this.hasPanelTarget) {
222
+ this.panelTarget.style.width = savedWidth + 'px'
223
+ }
224
+
225
+ // Bind methods for event listeners
226
+ this.boundOnMouseMove = this.onMouseMove.bind(this)
227
+ this.boundOnMouseUp = this.onMouseUp.bind(this)
228
+ }
229
+
230
+ startResize(event) {
231
+ event.preventDefault()
232
+ this.isResizing = true
233
+ this.startX = event.clientX
234
+ this.startWidth = this.panelTarget.offsetWidth
235
+
236
+ this.handleTarget.classList.add('dragging')
237
+ this.panelTarget.classList.add('resizing')
238
+ document.body.style.cursor = 'col-resize'
239
+ document.body.style.userSelect = 'none'
240
+
241
+ document.addEventListener('mousemove', this.boundOnMouseMove)
242
+ document.addEventListener('mouseup', this.boundOnMouseUp)
243
+ }
244
+
245
+ onMouseMove(event) {
246
+ if (!this.isResizing) return
247
+
248
+ const deltaX = this.startX - event.clientX
249
+ let newWidth = this.startWidth + deltaX
250
+
251
+ // Clamp width between min and max
252
+ newWidth = Math.max(this.minWidth, Math.min(this.maxWidth, newWidth))
253
+
254
+ this.panelTarget.style.width = newWidth + 'px'
255
+ }
256
+
257
+ onMouseUp() {
258
+ if (!this.isResizing) return
259
+
260
+ this.isResizing = false
261
+ this.handleTarget.classList.remove('dragging')
262
+ this.panelTarget.classList.remove('resizing')
263
+ document.body.style.cursor = ''
264
+ document.body.style.userSelect = ''
265
+
266
+ // Save width to localStorage
267
+ localStorage.setItem('leva-panel-right-width', this.panelTarget.offsetWidth)
268
+
269
+ document.removeEventListener('mousemove', this.boundOnMouseMove)
270
+ document.removeEventListener('mouseup', this.boundOnMouseUp)
271
+ }
272
+
273
+ disconnect() {
274
+ document.removeEventListener('mousemove', this.boundOnMouseMove)
275
+ document.removeEventListener('mouseup', this.boundOnMouseUp)
276
+ }
277
+ })
278
+
279
+ application.register("collapsible-panel", class extends Stimulus.Controller {
280
+ static values = { key: String }
281
+
282
+ connect() {
283
+ this.widthKey = 'leva-panel-right-width'
284
+ const isCollapsed = localStorage.getItem(this.keyValue) === 'true'
285
+
286
+ if (isCollapsed) {
287
+ this.element.classList.add('collapsed')
288
+ } else {
289
+ // Restore saved width when not collapsed
290
+ const savedWidth = localStorage.getItem(this.widthKey)
291
+ if (savedWidth) {
292
+ this.element.style.width = savedWidth + 'px'
293
+ }
294
+ }
295
+ }
296
+
297
+ toggle() {
298
+ const isCurrentlyCollapsed = this.element.classList.contains('collapsed')
299
+
300
+ if (isCurrentlyCollapsed) {
301
+ // Expanding - restore the saved width
302
+ this.element.classList.remove('collapsed')
303
+ const savedWidth = localStorage.getItem(this.widthKey)
304
+ if (savedWidth) {
305
+ this.element.style.width = savedWidth + 'px'
306
+ }
307
+ } else {
308
+ // Collapsing - save current width first, then collapse
309
+ const currentWidth = this.element.offsetWidth
310
+ if (currentWidth > 48) {
311
+ localStorage.setItem(this.widthKey, currentWidth)
312
+ }
313
+ this.element.classList.add('collapsed')
314
+ }
315
+
316
+ const isNowCollapsed = this.element.classList.contains('collapsed')
317
+ localStorage.setItem(this.keyValue, isNowCollapsed)
156
318
  }
157
319
  })
158
320
  })()
159
- </script>
321
+ </script>
@@ -1,10 +1,20 @@
1
- <div class="bg-gray-900 p-4 flex items-center justify-between border-b border-gray-800">
2
- <div>
1
+ <div class="content-header">
2
+ <div class="header-inline">
3
3
  <% if selected_prompt.present? %>
4
- <span class="font-medium text-lg"><%= selected_prompt.name %></span>
5
- <span class="text-sm text-indigo-300 ml-2">v<%= selected_prompt.version %></span>
4
+ <h1 class="content-header-title"><%= selected_prompt.name %></h1>
5
+ <span class="badge badge-default" style="font-family: var(--font-mono); font-size: 11px;">v<%= selected_prompt.version %></span>
6
6
  <% else %>
7
- <span class="font-medium text-lg text-gray-400">No prompt selected</span>
7
+ <span class="content-header-title text-muted">Select a prompt to begin</span>
8
8
  <% end %>
9
9
  </div>
10
- </div>
10
+ <% if selected_prompt.present? %>
11
+ <div class="flex items-center gap-2">
12
+ <%= link_to edit_workbench_path(selected_prompt), class: "btn btn-ghost btn-sm" do %>
13
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor">
14
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" />
15
+ </svg>
16
+ Edit
17
+ <% end %>
18
+ </div>
19
+ <% end %>
20
+ </div>
@@ -1,20 +1,51 @@
1
1
  <% content_for :title, "Edit Prompt: #{@prompt.name}" %>
2
- <div class="container mx-auto px-4 py-8 bg-gray-950 text-white">
3
- <h1 class="text-3xl font-bold text-indigo-400 mb-6">Edit Prompt: <%= @prompt.name %></h1>
4
- <%= form_with(model: @prompt, url: workbench_path(@prompt), method: :patch, local: false, class: "bg-gray-800 rounded-lg shadow-lg p-6", data: { controller: "prompt-form" }) do |form| %>
5
- <div class="mb-4">
6
- <%= form.label :name, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
7
- <%= form.text_field :name, class: "w-full bg-gray-700 text-white p-3 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none", data: { action: "input->prompt-form#autoSave" } %>
2
+ <div class="container page">
3
+ <div class="page-header">
4
+ <div class="flex items-center gap-3">
5
+ <%= link_to workbench_index_path(prompt_id: @prompt.id), class: "btn btn-ghost btn-sm" do %>
6
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor">
7
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18" />
8
+ </svg>
9
+ <% end %>
10
+ <div>
11
+ <h1 class="page-title" style="margin-bottom: 0;">Edit Prompt</h1>
12
+ <p class="text-sm text-muted" style="margin: 0;"><%= @prompt.name %></p>
13
+ </div>
8
14
  </div>
9
- <div class="mb-4">
10
- <%= form.label :system_prompt, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
11
- <%= form.text_area :system_prompt, rows: 5, class: "w-full bg-gray-700 text-white p-3 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none", data: { action: "input->prompt-form#autoSave" } %>
15
+ <span class="badge badge-default" style="font-family: var(--font-mono); font-size: 11px;">v<%= @prompt.version %></span>
16
+ </div>
17
+
18
+ <%= form_with(model: @prompt, url: workbench_path(@prompt), method: :patch, local: false, class: "card", data: { controller: "prompt-form" }) do |form| %>
19
+ <div class="form-group">
20
+ <div class="flex items-center gap-2 mb-2">
21
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
22
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 7h.01M7 3h5c.512 0 1.024.195 1.414.586l7 7a2 2 0 010 2.828l-7 7a2 2 0 01-2.828 0l-7-7A1.994 1.994 0 013 12V7a4 4 0 014-4z" />
23
+ </svg>
24
+ <%= form.label :name, class: "form-label", style: "margin-bottom: 0;" %>
25
+ </div>
26
+ <%= form.text_field :name, class: "form-input", data: { action: "input->prompt-form#autoSave" } %>
12
27
  </div>
13
- <div class="mb-4">
14
- <%= form.label :user_prompt, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
15
- <%= form.text_area :user_prompt, rows: 5, class: "w-full bg-gray-700 text-white p-3 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none", data: { action: "input->prompt-form#autoSave" } %>
28
+
29
+ <div class="form-group">
30
+ <div class="flex items-center gap-2 mb-2">
31
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
32
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.75 17L9 20l-1 1h8l-1-1-.75-3M3 13h18M5 17h14a2 2 0 002-2V5a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
33
+ </svg>
34
+ <%= form.label :system_prompt, "System Prompt", class: "form-label", style: "margin-bottom: 0;" %>
35
+ </div>
36
+ <%= form.text_area :system_prompt, rows: 5, class: "form-textarea prompt-textarea", placeholder: "Define the AI's role and behavior...", data: { action: "input->prompt-form#autoSave" } %>
16
37
  </div>
17
- <div id="form-status" class="mb-4 text-center"></div>
38
+
39
+ <div class="form-group">
40
+ <div class="flex items-center gap-2 mb-2">
41
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
42
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" />
43
+ </svg>
44
+ <%= form.label :user_prompt, "User Prompt Template", class: "form-label", style: "margin-bottom: 0;" %>
45
+ </div>
46
+ <%= form.text_area :user_prompt, rows: 8, class: "form-textarea prompt-textarea", placeholder: "Use {{ variable }} syntax for dynamic content...", data: { action: "input->prompt-form#autoSave" } %>
47
+ </div>
48
+
49
+ <div id="form-status" class="autosave-status" data-prompt-form-target="status"></div>
18
50
  <% end %>
19
- <%= link_to "Back to Workbench", workbench_index_path, class: "btn btn-secondary" %>
20
- </div>
51
+ </div>
@@ -1,15 +1,12 @@
1
1
  <% content_for :title, 'Workbench' %>
2
- <div class="flex h-[calc(100vh-4rem)] bg-gray-950 text-white">
3
- <!-- Left Sidebar -->
2
+ <div class="layout-workbench">
4
3
  <%= render 'prompt_sidebar', prompts: @prompts, selected_prompt: @selected_prompt %>
5
- <!-- Main Content -->
6
- <div class="flex-1 flex flex-col">
7
- <!-- Top Bar -->
4
+ <div class="main-content">
8
5
  <%= render 'top_bar', selected_prompt: @selected_prompt %>
9
- <!-- Scrollable Content -->
10
- <div class="flex-1 flex overflow-hidden">
6
+ <div class="content-body" data-controller="resizable">
11
7
  <%= render 'prompt_content', selected_prompt: @selected_prompt %>
8
+ <div class="resize-handle" data-resizable-target="handle" data-action="mousedown->resizable#startResize"></div>
12
9
  <%= render 'results_section', evaluators: @evaluators, dataset_record: @dataset_record %>
13
10
  </div>
14
11
  </div>
15
- </div>
12
+ </div>