leva 0.1.5 → 0.1.7

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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -16
  3. data/app/controllers/leva/dataset_records_controller.rb +21 -0
  4. data/app/controllers/leva/datasets_controller.rb +9 -2
  5. data/app/controllers/leva/experiments_controller.rb +34 -9
  6. data/app/controllers/leva/runner_results_controller.rb +8 -0
  7. data/app/controllers/leva/workbench_controller.rb +85 -12
  8. data/app/helpers/leva/application_helper.rb +39 -0
  9. data/app/javascript/controllers/prompt_form_controller.js +45 -0
  10. data/app/javascript/controllers/prompt_selector_controller.js +31 -0
  11. data/app/jobs/leva/experiment_job.rb +9 -4
  12. data/app/jobs/leva/run_eval_job.rb +40 -0
  13. data/app/models/concerns/leva/recordable.rb +37 -0
  14. data/app/models/leva/dataset.rb +15 -6
  15. data/app/models/leva/dataset_record.rb +40 -1
  16. data/app/models/leva/evaluation_result.rb +15 -7
  17. data/app/models/leva/experiment.rb +24 -12
  18. data/app/models/leva/prompt.rb +14 -1
  19. data/app/models/leva/runner_result.rb +56 -0
  20. data/app/views/layouts/leva/application.html.erb +24 -13
  21. data/app/views/leva/dataset_records/index.html.erb +49 -0
  22. data/app/views/leva/dataset_records/show.html.erb +30 -0
  23. data/app/views/leva/datasets/_dataset.html.erb +18 -0
  24. data/app/views/leva/datasets/_form.html.erb +24 -0
  25. data/app/views/leva/datasets/edit.html.erb +5 -0
  26. data/app/views/leva/datasets/index.html.erb +51 -38
  27. data/app/views/leva/datasets/new.html.erb +5 -0
  28. data/app/views/leva/datasets/show.html.erb +160 -8
  29. data/app/views/leva/experiments/_experiment.html.erb +42 -0
  30. data/app/views/leva/experiments/_form.html.erb +49 -0
  31. data/app/views/leva/experiments/edit.html.erb +5 -0
  32. data/app/views/leva/experiments/index.html.erb +53 -37
  33. data/app/views/leva/experiments/new.html.erb +5 -0
  34. data/app/views/leva/experiments/show.html.erb +115 -19
  35. data/app/views/leva/runner_results/show.html.erb +64 -0
  36. data/app/views/leva/workbench/_evaluation_area.html.erb +5 -0
  37. data/app/views/leva/workbench/_prompt_content.html.erb +216 -0
  38. data/app/views/leva/workbench/_prompt_form.html.erb +89 -0
  39. data/app/views/leva/workbench/_prompt_sidebar.html.erb +21 -0
  40. data/app/views/leva/workbench/_results_section.html.erb +159 -0
  41. data/app/views/leva/workbench/_top_bar.html.erb +10 -0
  42. data/app/views/leva/workbench/edit.html.erb +20 -0
  43. data/app/views/leva/workbench/index.html.erb +5 -91
  44. data/app/views/leva/workbench/new.html.erb +79 -36
  45. data/config/routes.rb +15 -6
  46. data/db/migrate/20240813172916_create_leva_datasets.rb +1 -0
  47. data/db/migrate/20240813173035_create_leva_experiments.rb +1 -0
  48. data/db/migrate/20240816201419_create_leva_runner_results.rb +11 -0
  49. data/db/migrate/20240816201433_update_leva_evaluation_results.rb +8 -0
  50. data/db/migrate/20240821163608_make_experiment_optional_for_runner_results.rb +6 -0
  51. data/db/migrate/20240821181934_add_prompt_to_leva_runner_results.rb +5 -0
  52. data/db/migrate/20240821183153_add_runner_and_evaluator_to_leva_experiments.rb +6 -0
  53. data/db/migrate/20240821191713_add_actual_result_to_leva_dataset_records.rb +5 -0
  54. data/db/migrate/20240822143201_remove_actual_result_from_leva_runner_results.rb +5 -0
  55. data/db/migrate/20240912183556_add_runner_class_to_leva_runner_results.rb +5 -0
  56. data/lib/generators/leva/templates/eval.rb.erb +7 -8
  57. data/lib/generators/leva/templates/runner.rb.erb +25 -0
  58. data/lib/leva/version.rb +1 -1
  59. data/lib/leva.rb +84 -44
  60. metadata +49 -5
  61. data/app/evals/test_sentiment_accuracy_eval.rb +0 -6
  62. data/app/runners/test_sentiment_run.rb +0 -13
  63. data/lib/leva/base_eval.rb +0 -75
@@ -0,0 +1,216 @@
1
+ <% if @selected_prompt %>
2
+ <div class="flex-1 overflow-y-auto p-6 space-y-6" data-controller="prompt-autosave collapsible clipboard" data-prompt-autosave-url-value="<%= workbench_path(@selected_prompt) %>">
3
+ <!-- System Prompt -->
4
+ <div class="bg-gray-900 p-5 rounded-lg shadow-lg">
5
+ <div class="flex justify-between items-center mb-3">
6
+ <h2 class="text-sm font-semibold text-indigo-400">SYSTEM PROMPT</h2>
7
+ <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="systemPrompt">
8
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
10
+ </svg>
11
+ Copy
12
+ </button>
13
+ </div>
14
+ <textarea
15
+ class="w-full bg-gray-800 text-white p-3 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none min-h-[100px] overflow-hidden resize-none"
16
+ name="prompt[system_prompt]"
17
+ data-prompt-autosave-target="input"
18
+ id="systemPrompt"
19
+ data-action="input->prompt-autosave#debouncedSave"
20
+ ><%= @selected_prompt.system_prompt %></textarea>
21
+ </div>
22
+ <!-- User Message (Prompt Template) -->
23
+ <div class="bg-gray-900 p-5 rounded-lg shadow-lg">
24
+ <div class="flex justify-between items-center mb-3">
25
+ <h2 class="text-sm font-semibold text-indigo-400">USER (PROMPT TEMPLATE)</h2>
26
+ <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="userPrompt">
27
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
28
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
29
+ </svg>
30
+ Copy
31
+ </button>
32
+ </div>
33
+ <textarea
34
+ class="w-full bg-gray-800 text-white p-3 rounded-lg focus:ring-2 focus:ring-indigo-500 focus:outline-none min-h-[200px] overflow-hidden resize-none"
35
+ name="prompt[user_prompt]"
36
+ data-prompt-autosave-target="input"
37
+ id="userPrompt"
38
+ data-action="input->prompt-autosave#debouncedSave"
39
+ ><%= @selected_prompt.user_prompt %></textarea>
40
+ </div>
41
+ <!-- Available Liquid Tags -->
42
+ <% if @dataset_record && @dataset_record.recordable.respond_to?(:to_llm_context) %>
43
+ <div class="bg-gray-900 p-5 rounded-lg shadow-lg" data-controller="collapsible">
44
+ <h2 class="text-sm font-semibold mb-3 text-indigo-400 cursor-pointer flex items-center" data-action="click->collapsible#toggle">
45
+ AVAILABLE LIQUID TAGS
46
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
47
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
48
+ </svg>
49
+ </h2>
50
+ <div class="bg-gray-800 p-3 rounded-lg text-sm hidden" data-collapsible-target="content">
51
+ <% @dataset_record.recordable.to_llm_context.each do |key, value| %>
52
+ <details class="mb-2">
53
+ <summary class="text-green-400 cursor-pointer flex items-center justify-between">
54
+ <span>{{ <%= key %> }}</span>
55
+ <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">
56
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
57
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
58
+ </svg>
59
+ Copy
60
+ </button>
61
+ </summary>
62
+ <pre class="text-xs text-gray-300 mt-1 whitespace-pre-wrap" id="liquidTag<%= key %>"><%= value.to_s %></pre>
63
+ </details>
64
+ <% end %>
65
+ </div>
66
+ </div>
67
+ <% end %>
68
+ <!-- Full Prompt Preview -->
69
+ <% if @dataset_record %>
70
+ <div class="bg-gray-900 p-5 rounded-lg shadow-lg">
71
+ <div class="flex justify-between items-center mb-3">
72
+ <h2 class="text-sm font-semibold text-indigo-400">FULL PROMPT PREVIEW</h2>
73
+ <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="fullPrompt">
74
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
75
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3" />
76
+ </svg>
77
+ Copy
78
+ </button>
79
+ </div>
80
+ <pre class="w-full bg-gray-800 text-white p-3 rounded-lg text-sm whitespace-pre-wrap" id="fullPrompt"><%= Liquid::Template.parse(@selected_prompt.user_prompt).render(@dataset_record.recordable.to_llm_context.stringify_keys) %></pre>
81
+ </div>
82
+ <% end %>
83
+ <div class="text-sm text-center" data-prompt-autosave-target="status"></div>
84
+ </div>
85
+ <script>
86
+ (() => {
87
+ const application = Stimulus.Application.start()
88
+
89
+ application.register("prompt-autosave", class extends Stimulus.Controller {
90
+ static targets = ["input", "status"]
91
+ static values = { url: String }
92
+
93
+ connect() {
94
+ this.debouncedSave = this.debounce(this.save.bind(this), 1000)
95
+ this.adjustTextareaHeight()
96
+ this.inputTargets.forEach(input => {
97
+ input.addEventListener('input', () => this.adjustTextareaHeight(input))
98
+ })
99
+ }
100
+
101
+ adjustTextareaHeight(textarea = null) {
102
+ const textareas = textarea ? [textarea] : this.inputTargets
103
+ textareas.forEach(ta => {
104
+ ta.style.height = 'auto'
105
+ ta.style.height = ta.scrollHeight + 'px'
106
+ })
107
+ }
108
+
109
+ debouncedSave() {
110
+ this.debouncedSave()
111
+ }
112
+
113
+ save() {
114
+ const data = new FormData()
115
+ this.inputTargets.forEach(input => {
116
+ data.append(input.name, input.value)
117
+ })
118
+
119
+ this.statusTarget.textContent = "Saving..."
120
+ this.statusTarget.classList.add("text-yellow-500")
121
+
122
+ fetch(this.urlValue, {
123
+ method: 'PATCH',
124
+ body: data,
125
+ headers: {
126
+ 'X-CSRF-Token': document.querySelector('meta[name="csrf-token"]').content,
127
+ 'Accept': 'application/json'
128
+ },
129
+ credentials: 'same-origin'
130
+ })
131
+ .then(response => response.json())
132
+ .then(data => {
133
+ if (data.status === 'success') {
134
+ this.statusTarget.textContent = "Changes saved successfully"
135
+ this.statusTarget.classList.remove("text-yellow-500")
136
+ this.statusTarget.classList.add("text-green-500")
137
+ } else {
138
+ this.statusTarget.textContent = `Error: ${data.errors.join(", ")}`
139
+ this.statusTarget.classList.remove("text-yellow-500")
140
+ this.statusTarget.classList.add("text-red-500")
141
+ }
142
+ setTimeout(() => {
143
+ this.statusTarget.textContent = ""
144
+ this.statusTarget.classList.remove("text-green-500", "text-red-500")
145
+ }, 3000)
146
+ })
147
+ .catch(error => {
148
+ console.error('Error:', error)
149
+ this.statusTarget.textContent = "Error saving changes"
150
+ this.statusTarget.classList.remove("text-yellow-500")
151
+ this.statusTarget.classList.add("text-red-500")
152
+ })
153
+ }
154
+
155
+ debounce(func, wait) {
156
+ let timeout
157
+ return function executedFunction(...args) {
158
+ const later = () => {
159
+ clearTimeout(timeout)
160
+ func(...args)
161
+ }
162
+ clearTimeout(timeout)
163
+ timeout = setTimeout(later, wait)
164
+ }
165
+ }
166
+ })
167
+
168
+ application.register("collapsible", class extends Stimulus.Controller {
169
+ static targets = ["content"]
170
+
171
+ toggle() {
172
+ this.contentTarget.classList.toggle("hidden")
173
+ }
174
+ })
175
+
176
+ application.register("clipboard", class extends Stimulus.Controller {
177
+ static targets = ["content"]
178
+
179
+ copy(event) {
180
+ const sourceId = event.currentTarget.dataset.clipboardSource
181
+ const sourceElement = document.getElementById(sourceId)
182
+ const content = sourceElement.value || sourceElement.textContent
183
+ navigator.clipboard.writeText(content).then(() => {
184
+ this.showFeedback(event.currentTarget, "Copied!")
185
+ }, (err) => {
186
+ console.error('Could not copy text: ', err)
187
+ this.showFeedback(event.currentTarget, "Failed to copy", true)
188
+ })
189
+ }
190
+
191
+ showFeedback(button, message, isError = false) {
192
+ const originalText = button.textContent
193
+ button.textContent = message
194
+ button.classList.add(isError ? "text-red-500" : "text-green-500")
195
+ button.disabled = true
196
+
197
+ setTimeout(() => {
198
+ button.textContent = originalText
199
+ button.classList.remove("text-green-500", "text-red-500")
200
+ button.disabled = false
201
+ }, 2000)
202
+ }
203
+ })
204
+ })()
205
+ </script>
206
+ <% else %>
207
+ <div class="flex-1 flex items-center justify-center">
208
+ <div class="text-center text-gray-500">
209
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-12 w-12 mx-auto mb-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
210
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4" />
211
+ </svg>
212
+ <h3 class="text-xl font-medium mb-2">No Prompt Selected</h3>
213
+ <p class="text-sm">Please select a prompt from the sidebar to begin editing.</p>
214
+ </div>
215
+ </div>
216
+ <% end %>
@@ -0,0 +1,89 @@
1
+ <%= 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| %>
2
+ <div class="mb-4">
3
+ <%= form.label :name, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
4
+ <%= 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" } %>
5
+ </div>
6
+ <div class="mb-4">
7
+ <%= form.label :version, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
8
+ <%= form.number_field :version, 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" } %>
9
+ </div>
10
+ <div class="mb-4">
11
+ <%= form.label :system_prompt, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
12
+ <%= 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" } %>
13
+ </div>
14
+ <div class="mb-4">
15
+ <%= form.label :user_prompt, class: "block text-sm font-semibold mb-2 text-indigo-300" %>
16
+ <%= 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" } %>
17
+ </div>
18
+ <div id="form-status" class="mb-4 text-center" data-prompt-form-target="status"></div>
19
+ <% end %>
20
+ <script>
21
+ (() => {
22
+ const application = Stimulus.Application.start()
23
+
24
+ application.register("prompt-form", class extends Stimulus.Controller {
25
+ static targets = ["status"]
26
+
27
+ connect() {
28
+ this.timeout = null
29
+ this.debounceTime = 1000 // 1 second debounce
30
+ this.lastSavedContent = this.formContent()
31
+ }
32
+
33
+ autoSave() {
34
+ clearTimeout(this.timeout)
35
+ this.timeout = setTimeout(() => {
36
+ const currentContent = this.formContent()
37
+ if (currentContent !== this.lastSavedContent) {
38
+ this.submitForm()
39
+ } else {
40
+ this.showStatus("No changes to save", "text-gray-500")
41
+ }
42
+ }, this.debounceTime)
43
+ }
44
+
45
+ submitForm() {
46
+ const form = this.element
47
+ const formData = new FormData(form)
48
+
49
+ this.showStatus("Saving...", "text-yellow-500")
50
+
51
+ fetch(form.action, {
52
+ method: form.method,
53
+ body: formData,
54
+ headers: {
55
+ "Accept": "application/json",
56
+ "X-Requested-With": "XMLHttpRequest",
57
+ "X-CSRF-Token": document.querySelector('meta[name="csrf-token"]').content
58
+ },
59
+ })
60
+ .then(response => response.json())
61
+ .then(data => {
62
+ if (data.status === "success") {
63
+ this.showStatus("Changes saved successfully", "text-green-500")
64
+ this.lastSavedContent = this.formContent()
65
+ } else {
66
+ this.showStatus(`Error: ${data.errors.join(", ")}`, "text-red-500")
67
+ }
68
+ })
69
+ .catch(error => {
70
+ console.error("Error:", error)
71
+ this.showStatus("Error saving changes", "text-red-500")
72
+ })
73
+ }
74
+
75
+ showStatus(message, className) {
76
+ this.statusTarget.textContent = message
77
+ this.statusTarget.className = `mb-4 text-center ${className}`
78
+ setTimeout(() => {
79
+ this.statusTarget.textContent = ""
80
+ this.statusTarget.className = "mb-4 text-center"
81
+ }, 3000)
82
+ }
83
+
84
+ formContent() {
85
+ return JSON.stringify(Object.fromEntries(new FormData(this.element)))
86
+ }
87
+ })
88
+ })()
89
+ </script>
@@ -0,0 +1,21 @@
1
+ <div class="w-64 h-full bg-gray-900 border-r border-gray-800 flex flex-col">
2
+ <div class="p-4">
3
+ <h2 class="text-xl font-bold mb-4 text-indigo-400">Prompts</h2>
4
+ <div class="space-y-2">
5
+ <% prompts.each do |prompt| %>
6
+ <%= link_to workbench_index_path(prompt_id: prompt.id), class: "block bg-gray-800 p-3 rounded-lg hover:bg-gray-700 transition duration-150 ease-in-out #{'bg-indigo-600' if prompt == selected_prompt}" do %>
7
+ <span class="text-sm font-medium"><%= prompt.name %></span>
8
+ <span class="text-xs text-indigo-300 ml-2">v<%= prompt.version %></span>
9
+ <% end %>
10
+ <% end %>
11
+ </div>
12
+ </div>
13
+ <div class="mt-auto p-4">
14
+ <%= link_to new_workbench_path, class: "w-full flex items-center justify-center space-x-2 bg-indigo-600 hover:bg-indigo-700 p-3 rounded-lg transition duration-150 ease-in-out" do %>
15
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
16
+ <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
17
+ </svg>
18
+ <span>New Prompt</span>
19
+ <% end %>
20
+ </div>
21
+ </div>
@@ -0,0 +1,159 @@
1
+ <div class="w-1/2 bg-gray-900 border-l border-gray-800 p-5 overflow-y-auto" 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" %>
24
+ <%= hidden_field_tag :prompt_id, @prompt&.id %>
25
+ <%= hidden_field_tag :runner, @selected_runner %>
26
+ <% 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">
33
+ <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
+ </svg>
35
+ Run
36
+ </span>
37
+ <% end %>
38
+ </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 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 bg-gray-700 p-2 rounded"><%= runner_result.prediction %></pre>
50
+ </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 bg-gray-700 p-2 rounded mb-2"><%= prediction %></pre>
56
+ <% end %>
57
+ </div>
58
+ <% 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>
62
+ </div>
63
+ <% 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" />
67
+ </svg>
68
+ <p class="text-sm text-gray-500">No results yet. Click 'Run' to start the analysis.</p>
69
+ </div>
70
+ <% end %>
71
+ </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>
83
+ <% end %>
84
+ </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) %>
112
+ </div>
113
+ <% else %>
114
+ <div class="text-sm text-gray-500">
115
+ No evaluation result yet.
116
+ </div>
117
+ <% end %>
118
+ <% end %>
119
+ </div>
120
+ <% end %>
121
+ </div>
122
+ </div>
123
+ </div>
124
+ <script>
125
+ (() => {
126
+ const application = Stimulus.Application.start()
127
+
128
+ application.register("button-loader", class extends Stimulus.Controller {
129
+ static targets = ["button", "buttonText"]
130
+
131
+ handleClick(event) {
132
+ event.preventDefault()
133
+ const button = event.currentTarget
134
+ const form = button.closest('form')
135
+
136
+ this.disableButton(button)
137
+ this.showSpinner(button)
138
+
139
+ // Submit the form
140
+ form.submit()
141
+ }
142
+
143
+ disableButton(button) {
144
+ button.disabled = true
145
+ button.classList.add('opacity-50', 'cursor-not-allowed')
146
+ }
147
+
148
+ showSpinner(button) {
149
+ 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
+ `
156
+ }
157
+ })
158
+ })()
159
+ </script>
@@ -0,0 +1,10 @@
1
+ <div class="bg-gray-900 p-4 flex items-center justify-between border-b border-gray-800">
2
+ <div>
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>
6
+ <% else %>
7
+ <span class="font-medium text-lg text-gray-400">No prompt selected</span>
8
+ <% end %>
9
+ </div>
10
+ </div>
@@ -0,0 +1,20 @@
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" } %>
8
+ </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" } %>
12
+ </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" } %>
16
+ </div>
17
+ <div id="form-status" class="mb-4 text-center"></div>
18
+ <% end %>
19
+ <%= link_to "Back to Workbench", workbench_index_path, class: "btn btn-secondary" %>
20
+ </div>
@@ -1,101 +1,15 @@
1
1
  <% content_for :title, 'Workbench' %>
2
- <div class="flex h-[calc(100vh-4rem)] bg-gray-900 text-white">
2
+ <div class="flex h-[calc(100vh-4rem)] bg-gray-950 text-white">
3
3
  <!-- Left Sidebar -->
4
- <div class="w-64 h-full bg-gray-800 border-r border-gray-700 flex flex-col">
5
- <div class="p-4">
6
- <h2 class="text-xl font-bold mb-4">Prompts</h2>
7
- <div class="space-y-2">
8
- <% @prompts.each do |prompt| %>
9
- <div class="bg-gray-700 p-2 rounded">
10
- <span class="text-sm font-medium"><%= prompt.name %></span>
11
- <span class="text-xs text-gray-400 ml-2">v<%= prompt.version %></span>
12
- </div>
13
- <% end %>
14
- </div>
15
- </div>
16
- <div class="mt-auto p-4">
17
- <%= link_to new_prompt_path, class: "w-full flex items-center justify-center space-x-2 bg-indigo-600 hover:bg-indigo-700 p-2 rounded" do %>
18
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
19
- <path fill-rule="evenodd" d="M10 3a1 1 0 011 1v5h5a1 1 0 110 2h-5v5a1 1 0 11-2 0v-5H4a1 1 0 110-2h5V4a1 1 0 011-1z" clip-rule="evenodd" />
20
- </svg>
21
- <span>New Prompt</span>
22
- <% end %>
23
- </div>
24
- </div>
4
+ <%= render 'prompt_sidebar', prompts: @prompts, selected_prompt: @selected_prompt %>
25
5
  <!-- Main Content -->
26
6
  <div class="flex-1 flex flex-col">
27
7
  <!-- Top Bar -->
28
- <div class="bg-gray-800 p-4 flex items-center justify-between border-b border-gray-700">
29
- <div>
30
- <span class="font-medium"><%= @selected_prompt.name %></span>
31
- <span class="text-xs text-gray-400 ml-2">v<%= @selected_prompt.version %></span>
32
- </div>
33
- <div class="flex items-center space-x-2">
34
- <%= button_to run_workbench_index_path, method: :post, class: "flex items-center space-x-2 px-4 py-2 rounded bg-indigo-600 hover:bg-indigo-700" do %>
35
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
36
- <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" />
37
- </svg>
38
- <span>Run</span>
39
- <% end %>
40
- <%= button_to run_with_evaluation_workbench_index_path, method: :post, class: "flex items-center space-x-2 px-4 py-2 rounded bg-green-600 hover:bg-green-700" do %>
41
- <svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
42
- <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" />
43
- </svg>
44
- <span>Run + Evaluate</span>
45
- <% end %>
46
- </div>
47
- </div>
8
+ <%= render 'top_bar', selected_prompt: @selected_prompt %>
48
9
  <!-- Scrollable Content -->
49
10
  <div class="flex-1 flex overflow-hidden">
50
- <div class="flex-1 overflow-y-auto p-6 space-y-4">
51
- <!-- System Prompt -->
52
- <div class="bg-gray-800 p-4 rounded">
53
- <h2 class="text-sm font-semibold mb-2">SYSTEM PROMPT</h2>
54
- <textarea
55
- class="w-full bg-gray-700 text-white p-2 rounded resize-none"
56
- style="height: auto; min-height: 100px;"
57
- name="system_prompt"
58
- ><%= @selected_prompt.system_prompt %></textarea>
59
- </div>
60
- <!-- User Message -->
61
- <div class="bg-gray-800 p-4 rounded">
62
- <h2 class="text-sm font-semibold mb-2">USER</h2>
63
- <textarea
64
- class="w-full bg-gray-700 text-white p-2 rounded resize-none"
65
- style="height: auto; min-height: 200px;"
66
- name="user_prompt"
67
- ><%= @selected_prompt.user_prompt %></textarea>
68
- </div>
69
- </div>
70
- <!-- Results Section -->
71
- <div class="w-1/3 bg-gray-800 border-l border-gray-700 p-4 overflow-y-auto">
72
- <h2 class="text-lg font-semibold mb-4">Results</h2>
73
- <!-- Run Result -->
74
- <div class="bg-gray-700 p-4 rounded mb-4">
75
- <h3 class="text-sm font-semibold mb-2">Run Result</h3>
76
- <p class="text-sm">
77
- <%= flash[:notice] || "No results yet. Click 'Run' or 'Run + Evaluate' to start the analysis." %>
78
- </p>
79
- </div>
80
- <!-- Evaluators -->
81
- <div class="space-y-4">
82
- <h3 class="text-sm font-semibold">Evaluators</h3>
83
- <% @evaluators.each do |evaluator| %>
84
- <div class="bg-gray-700 p-4 rounded">
85
- <div class="flex items-center justify-between mb-2">
86
- <span class="text-sm font-medium"><%= evaluator %></span>
87
- <div class="flex items-center space-x-2">
88
- <%= button_to run_evaluator_workbench_index_path, method: :post, params: { evaluator: evaluator }, class: "p-1 bg-blue-500 hover:bg-blue-600 rounded" do %>
89
- <svg xmlns="http://www.w3.org/2000/svg" class="h-3 w-3" viewBox="0 0 20 20" fill="currentColor">
90
- <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" />
91
- </svg>
92
- <% end %>
93
- </div>
94
- </div>
95
- </div>
96
- <% end %>
97
- </div>
98
- </div>
11
+ <%= render 'prompt_content', selected_prompt: @selected_prompt %>
12
+ <%= render 'results_section', evaluators: @evaluators, dataset_record: @dataset_record %>
99
13
  </div>
100
14
  </div>
101
15
  </div>