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,5 +1,19 @@
1
- <div class="bg-gray-800 rounded-lg shadow-lg p-6">
2
- <h3 class="text-xl font-semibold mb-4 text-indigo-300">Evaluation Results</h3>
3
- <!-- Add evaluation results display here -->
4
- <p class="text-gray-400">No evaluation results available yet. Run an evaluation to see results.</p>
1
+ <div class="card">
2
+ <div class="card-header">
3
+ <div class="flex items-center gap-2">
4
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
5
+ <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" />
6
+ </svg>
7
+ <h3 class="card-title">Evaluation Results</h3>
8
+ </div>
9
+ </div>
10
+ <div class="card-body">
11
+ <div class="empty-state-inline">
12
+ <svg class="icon-lg" style="margin: 0 auto var(--space-3); color: var(--gray-500);" fill="none" viewBox="0 0 24 24" stroke="currentColor">
13
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
14
+ </svg>
15
+ <p class="text-muted text-sm">No evaluation results available yet.</p>
16
+ <p class="text-subtle text-xs mt-2">Run an evaluation to see results here.</p>
17
+ </div>
18
+ </div>
5
19
  </div>
@@ -1,133 +1,146 @@
1
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">
2
+ <div class="panel" data-controller="prompt-autosave collapsible clipboard" data-prompt-autosave-url-value="<%= workbench_path(@selected_prompt) %>">
3
+ <%# System Prompt %>
4
+ <div class="panel-section" style="padding-bottom: var(--space-4); border-bottom: 1px solid var(--gray-800);">
5
+ <div class="label-inline" style="margin-bottom: var(--space-3);">
6
+ <div class="flex items-center gap-2">
7
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
8
+ <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" />
9
+ </svg>
10
+ <span class="panel-section-title">System Prompt</span>
11
+ </div>
12
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="systemPrompt" title="Copy system prompt">
13
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor">
9
14
  <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
15
  </svg>
11
- Copy
12
16
  </button>
13
17
  </div>
14
18
  <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-y-auto resize-none break-words"
19
+ class="prompt-textarea"
16
20
  name="prompt[system_prompt]"
17
21
  data-prompt-autosave-target="input"
18
22
  id="systemPrompt"
19
23
  data-action="input->prompt-autosave#debouncedSave"
24
+ placeholder="Define the AI's role and behavior..."
20
25
  ><%= @selected_prompt.system_prompt %></textarea>
21
26
  </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">
27
+
28
+ <%# User Prompt Template %>
29
+ <div class="panel-section" style="padding-top: var(--space-4); padding-bottom: var(--space-4); border-bottom: 1px solid var(--gray-800);">
30
+ <div class="label-inline" style="margin-bottom: var(--space-3);">
31
+ <div class="flex items-center gap-2">
32
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
33
+ <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" />
34
+ </svg>
35
+ <span class="panel-section-title">User Prompt Template</span>
36
+ </div>
37
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="userPrompt" title="Copy user prompt">
38
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor">
28
39
  <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
40
  </svg>
30
- Copy
31
41
  </button>
32
42
  </div>
33
43
  <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-y-auto resize-none break-words"
44
+ class="prompt-textarea"
35
45
  name="prompt[user_prompt]"
36
46
  data-prompt-autosave-target="input"
37
47
  id="userPrompt"
38
48
  data-action="input->prompt-autosave#debouncedSave"
49
+ placeholder="Use {{ variable }} syntax for dynamic content..."
50
+ style="min-height: 140px;"
39
51
  ><%= @selected_prompt.user_prompt %></textarea>
40
52
  </div>
41
- <!-- Available Liquid Tags -->
53
+
54
+ <%# Available Liquid Tags %>
42
55
  <% 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
- <% # Use merged context if available, otherwise just record context %>
52
- <% context_to_display = @merged_context || @dataset_record.recordable.to_llm_context %>
53
-
54
- <% if @record_context && @runner_context %>
55
- <!-- Record Context -->
56
- <div class="mb-4">
57
- <h3 class="text-xs font-semibold text-gray-400 mb-2">FROM RECORD:</h3>
58
- <% @record_context.each do |key, value| %>
59
- <details class="mb-2">
60
- <summary class="text-green-400 cursor-pointer flex items-center justify-between">
61
- <span>{{ <%= key %> }}</span>
62
- <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">
63
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
64
- <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" />
65
- </svg>
66
- Copy
67
- </button>
68
- </summary>
69
- <pre class="text-xs text-gray-300 mt-1 whitespace-pre-wrap break-words overflow-x-auto max-w-full" id="liquidTag<%= key %>"><%= value.to_s %></pre>
70
- </details>
71
- <% end %>
56
+ <div class="panel-section" style="padding-top: var(--space-4); padding-bottom: var(--space-4); border-bottom: 1px solid var(--gray-800);">
57
+ <details class="collapsible" data-controller="collapsible">
58
+ <summary class="collapsible-header">
59
+ <div class="flex items-center gap-2">
60
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor" style="color: var(--accent-400);">
61
+ <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" />
62
+ </svg>
63
+ <span class="collapsible-title">Available Variables</span>
72
64
  </div>
73
-
74
- <!-- Runner Context -->
75
- <% if @runner_context.any? %>
65
+ <svg class="collapsible-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor">
66
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 9l-7 7-7-7" />
67
+ </svg>
68
+ </summary>
69
+ <div class="collapsible-body">
70
+ <% context_to_display = @merged_context || @dataset_record.recordable.to_llm_context %>
71
+
72
+ <% if @record_context && @runner_context %>
73
+ <%# Record Context %>
76
74
  <div class="mb-4">
77
- <h3 class="text-xs font-semibold text-gray-400 mb-2">FROM RUNNER:</h3>
78
- <% @runner_context.each do |key, value| %>
75
+ <p class="text-xs uppercase text-muted font-semibold mb-2">From Record:</p>
76
+ <% @record_context.each do |key, value| %>
79
77
  <details class="mb-2">
80
- <summary class="text-yellow-400 cursor-pointer flex items-center justify-between">
81
- <span>{{ <%= key %> }}</span>
82
- <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">
83
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
84
- <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" />
85
- </svg>
86
- Copy
87
- </button>
78
+ <summary class="flex items-center justify-between cursor-pointer">
79
+ <span class="tag tag-green">{{ <%= key %> }}</span>
80
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">Copy</button>
88
81
  </summary>
89
- <pre class="text-xs text-gray-300 mt-1 whitespace-pre-wrap break-words overflow-x-auto max-w-full" id="liquidTag<%= key %>"><%= value.to_s %></pre>
82
+ <pre class="mt-2" id="liquidTag<%= key %>"><code><%= value.to_s %></code></pre>
90
83
  </details>
91
84
  <% end %>
92
85
  </div>
86
+
87
+ <%# Runner Context %>
88
+ <% if @runner_context.any? %>
89
+ <div>
90
+ <p class="text-xs uppercase text-muted font-semibold mb-2">From Runner:</p>
91
+ <% @runner_context.each do |key, value| %>
92
+ <details class="mb-2">
93
+ <summary class="flex items-center justify-between cursor-pointer">
94
+ <span class="tag tag-yellow">{{ <%= key %> }}</span>
95
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">Copy</button>
96
+ </summary>
97
+ <pre class="mt-2" id="liquidTag<%= key %>"><code><%= value.to_s %></code></pre>
98
+ </details>
99
+ <% end %>
100
+ </div>
101
+ <% end %>
102
+ <% else %>
103
+ <% context_to_display.each do |key, value| %>
104
+ <details class="mb-2">
105
+ <summary class="flex items-center justify-between cursor-pointer">
106
+ <span class="tag tag-green">{{ <%= key %> }}</span>
107
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">Copy</button>
108
+ </summary>
109
+ <pre class="mt-2" id="liquidTag<%= key %>"><code><%= value.to_s %></code></pre>
110
+ </details>
111
+ <% end %>
93
112
  <% end %>
94
- <% else %>
95
- <!-- Fallback to original display -->
96
- <% context_to_display.each do |key, value| %>
97
- <details class="mb-2">
98
- <summary class="text-green-400 cursor-pointer flex items-center justify-between">
99
- <span>{{ <%= key %> }}</span>
100
- <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="liquidTag<%= key %>">
101
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
102
- <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" />
103
- </svg>
104
- Copy
105
- </button>
106
- </summary>
107
- <pre class="text-xs text-gray-300 mt-1 whitespace-pre-wrap break-words overflow-x-auto max-w-full" id="liquidTag<%= key %>"><%= value.to_s %></pre>
108
- </details>
109
- <% end %>
110
- <% end %>
111
- </div>
113
+ </div>
114
+ </details>
112
115
  </div>
113
116
  <% end %>
114
- <!-- Full Prompt Preview -->
117
+
118
+ <%# Full Prompt Preview %>
115
119
  <% if @dataset_record %>
116
- <div class="bg-gray-900 p-5 rounded-lg shadow-lg">
117
- <div class="flex justify-between items-center mb-3">
118
- <h2 class="text-sm font-semibold text-indigo-400">FULL PROMPT PREVIEW</h2>
119
- <button class="btn btn-small text-blue-400 hover:text-blue-300 flex items-center" data-action="clipboard#copy" data-clipboard-source="fullPrompt">
120
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-1" fill="none" viewBox="0 0 24 24" stroke="currentColor">
120
+ <div class="panel-section" style="padding-top: var(--space-4);">
121
+ <div class="label-inline" style="margin-bottom: var(--space-3);">
122
+ <div class="flex items-center gap-2">
123
+ <svg class="icon-sm text-muted" fill="none" viewBox="0 0 24 24" stroke="currentColor">
124
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M15 12a3 3 0 11-6 0 3 3 0 016 0z" />
125
+ <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" />
126
+ </svg>
127
+ <span class="panel-section-title">Rendered Preview</span>
128
+ </div>
129
+ <button class="btn btn-ghost btn-sm" data-action="clipboard#copy" data-clipboard-source="fullPrompt" title="Copy rendered prompt">
130
+ <svg class="icon-sm" fill="none" viewBox="0 0 24 24" stroke="currentColor">
121
131
  <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" />
122
132
  </svg>
123
- Copy
124
133
  </button>
125
134
  </div>
126
- <pre class="w-full bg-gray-800 text-white p-3 rounded-lg text-sm whitespace-pre-wrap overflow-x-auto break-words max-w-full" id="fullPrompt"><%= Liquid::Template.parse(@selected_prompt.user_prompt).render((@merged_context || @dataset_record.recordable.to_llm_context).stringify_keys) %></pre>
135
+ <div class="result-block" style="background: var(--gray-850, #222120); border: 1px solid var(--gray-700); max-height: 200px; overflow-y: auto;">
136
+ <pre id="fullPrompt" class="result-value" style="color: var(--gray-300);"><%= Liquid::Template.parse(@selected_prompt.user_prompt).render((@merged_context || @dataset_record.recordable.to_llm_context).stringify_keys) %></pre>
137
+ </div>
127
138
  </div>
128
139
  <% end %>
129
- <div class="text-sm text-center" data-prompt-autosave-target="status"></div>
140
+
141
+ <div class="autosave-status" data-prompt-autosave-target="status"></div>
130
142
  </div>
143
+
131
144
  <script>
132
145
  (() => {
133
146
  const application = Stimulus.Application.start()
@@ -149,8 +162,6 @@
149
162
  textareas.forEach(ta => {
150
163
  ta.style.height = 'auto'
151
164
  ta.style.height = (ta.scrollHeight + 5) + 'px'
152
-
153
- // Ensure horizontal text wrapping
154
165
  ta.style.wordBreak = 'break-word'
155
166
  ta.style.wordWrap = 'break-word'
156
167
  })
@@ -167,7 +178,7 @@
167
178
  })
168
179
 
169
180
  this.statusTarget.textContent = "Saving..."
170
- this.statusTarget.classList.add("text-yellow-500")
181
+ this.statusTarget.style.color = "var(--warning-400)"
171
182
 
172
183
  fetch(this.urlValue, {
173
184
  method: 'PATCH',
@@ -181,24 +192,20 @@
181
192
  .then(response => response.json())
182
193
  .then(data => {
183
194
  if (data.status === 'success') {
184
- this.statusTarget.textContent = "Changes saved successfully"
185
- this.statusTarget.classList.remove("text-yellow-500")
186
- this.statusTarget.classList.add("text-green-500")
195
+ this.statusTarget.textContent = "Changes saved"
196
+ this.statusTarget.style.color = "var(--success-400)"
187
197
  } else {
188
198
  this.statusTarget.textContent = `Error: ${data.errors.join(", ")}`
189
- this.statusTarget.classList.remove("text-yellow-500")
190
- this.statusTarget.classList.add("text-red-500")
199
+ this.statusTarget.style.color = "var(--error-400)"
191
200
  }
192
201
  setTimeout(() => {
193
202
  this.statusTarget.textContent = ""
194
- this.statusTarget.classList.remove("text-green-500", "text-red-500")
195
203
  }, 3000)
196
204
  })
197
205
  .catch(error => {
198
206
  console.error('Error:', error)
199
207
  this.statusTarget.textContent = "Error saving changes"
200
- this.statusTarget.classList.remove("text-yellow-500")
201
- this.statusTarget.classList.add("text-red-500")
208
+ this.statusTarget.style.color = "var(--error-400)"
202
209
  })
203
210
  }
204
211
 
@@ -224,8 +231,6 @@
224
231
  })
225
232
 
226
233
  application.register("clipboard", class extends Stimulus.Controller {
227
- static targets = ["content"]
228
-
229
234
  copy(event) {
230
235
  const sourceId = event.currentTarget.dataset.clipboardSource
231
236
  const sourceElement = document.getElementById(sourceId)
@@ -234,19 +239,19 @@
234
239
  this.showFeedback(event.currentTarget, "Copied!")
235
240
  }, (err) => {
236
241
  console.error('Could not copy text: ', err)
237
- this.showFeedback(event.currentTarget, "Failed to copy", true)
242
+ this.showFeedback(event.currentTarget, "Failed", true)
238
243
  })
239
244
  }
240
245
 
241
246
  showFeedback(button, message, isError = false) {
242
247
  const originalText = button.textContent
243
248
  button.textContent = message
244
- button.classList.add(isError ? "text-red-500" : "text-green-500")
249
+ button.style.color = isError ? "var(--error-400)" : "var(--success-400)"
245
250
  button.disabled = true
246
251
 
247
252
  setTimeout(() => {
248
253
  button.textContent = originalText
249
- button.classList.remove("text-green-500", "text-red-500")
254
+ button.style.color = ""
250
255
  button.disabled = false
251
256
  }, 2000)
252
257
  }
@@ -254,13 +259,13 @@
254
259
  })()
255
260
  </script>
256
261
  <% else %>
257
- <div class="flex-1 flex items-center justify-center">
258
- <div class="text-center text-gray-500">
259
- <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">
260
- <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" />
262
+ <div class="panel flex items-center justify-center">
263
+ <div class="empty-state">
264
+ <svg class="empty-state-icon" fill="none" viewBox="0 0 24 24" stroke="currentColor">
265
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" 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" />
261
266
  </svg>
262
- <h3 class="text-xl font-medium mb-2">No Prompt Selected</h3>
263
- <p class="text-sm">Please select a prompt from the sidebar to begin editing.</p>
267
+ <h3 class="empty-state-title">No Prompt Selected</h3>
268
+ <p class="empty-state-description">Select a prompt from the sidebar to begin editing.</p>
264
269
  </div>
265
270
  </div>
266
- <% end %>
271
+ <% end %>
@@ -1,22 +1,23 @@
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" } %>
1
+ <%= form_with(model: prompt, url: workbench_path(prompt), method: :patch, local: false, class: "card", data: { controller: "prompt-form" }) do |form| %>
2
+ <div class="form-group">
3
+ <%= form.label :name, class: "form-label" %>
4
+ <%= form.text_field :name, class: "form-input", data: { action: "input->prompt-form#autoSave" } %>
5
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" } %>
6
+ <div class="form-group">
7
+ <%= form.label :version, class: "form-label" %>
8
+ <%= form.number_field :version, class: "form-input", data: { action: "input->prompt-form#autoSave" } %>
9
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" } %>
10
+ <div class="form-group">
11
+ <%= form.label :system_prompt, class: "form-label" %>
12
+ <%= form.text_area :system_prompt, rows: 5, class: "form-textarea", data: { action: "input->prompt-form#autoSave" } %>
13
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" } %>
14
+ <div class="form-group">
15
+ <%= form.label :user_prompt, class: "form-label" %>
16
+ <%= form.text_area :user_prompt, rows: 5, class: "form-textarea", data: { action: "input->prompt-form#autoSave" } %>
17
17
  </div>
18
- <div id="form-status" class="mb-4 text-center" data-prompt-form-target="status"></div>
18
+ <div id="form-status" class="text-sm text-center" data-prompt-form-target="status"></div>
19
19
  <% end %>
20
+
20
21
  <script>
21
22
  (() => {
22
23
  const application = Stimulus.Application.start()
@@ -26,7 +27,7 @@
26
27
 
27
28
  connect() {
28
29
  this.timeout = null
29
- this.debounceTime = 1000 // 1 second debounce
30
+ this.debounceTime = 1000
30
31
  this.lastSavedContent = this.formContent()
31
32
  }
32
33
 
@@ -37,7 +38,7 @@
37
38
  if (currentContent !== this.lastSavedContent) {
38
39
  this.submitForm()
39
40
  } else {
40
- this.showStatus("No changes to save", "text-gray-500")
41
+ this.showStatus("No changes to save", "text-muted")
41
42
  }
42
43
  }, this.debounceTime)
43
44
  }
@@ -46,7 +47,7 @@
46
47
  const form = this.element
47
48
  const formData = new FormData(form)
48
49
 
49
- this.showStatus("Saving...", "text-yellow-500")
50
+ this.showStatus("Saving...", "text-warning")
50
51
 
51
52
  fetch(form.action, {
52
53
  method: form.method,
@@ -60,24 +61,24 @@
60
61
  .then(response => response.json())
61
62
  .then(data => {
62
63
  if (data.status === "success") {
63
- this.showStatus("Changes saved successfully", "text-green-500")
64
+ this.showStatus("Changes saved", "text-success")
64
65
  this.lastSavedContent = this.formContent()
65
66
  } else {
66
- this.showStatus(`Error: ${data.errors.join(", ")}`, "text-red-500")
67
+ this.showStatus(`Error: ${data.errors.join(", ")}`, "text-error")
67
68
  }
68
69
  })
69
70
  .catch(error => {
70
71
  console.error("Error:", error)
71
- this.showStatus("Error saving changes", "text-red-500")
72
+ this.showStatus("Error saving changes", "text-error")
72
73
  })
73
74
  }
74
75
 
75
76
  showStatus(message, className) {
76
77
  this.statusTarget.textContent = message
77
- this.statusTarget.className = `mb-4 text-center ${className}`
78
+ this.statusTarget.className = `text-sm text-center ${className}`
78
79
  setTimeout(() => {
79
80
  this.statusTarget.textContent = ""
80
- this.statusTarget.className = "mb-4 text-center"
81
+ this.statusTarget.className = "text-sm text-center"
81
82
  }, 3000)
82
83
  }
83
84
 
@@ -86,4 +87,4 @@
86
87
  }
87
88
  })
88
89
  })()
89
- </script>
90
+ </script>
@@ -1,21 +1,66 @@
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">
1
+ <div class="sidebar" data-controller="collapsible-sidebar" data-collapsible-sidebar-key-value="leva-sidebar-collapsed">
2
+ <div class="sidebar-header">
3
+ <div class="flex items-center justify-between">
4
+ <h2 class="sidebar-title">Prompts</h2>
5
+ <div class="flex items-center gap-1">
6
+ <span class="text-xs text-muted font-mono" data-collapsible-sidebar-target="hideable"><%= prompts.count %></span>
7
+ <button type="button" class="sidebar-collapse-btn" data-action="click->collapsible-sidebar#toggle" title="Toggle sidebar">
8
+ <svg viewBox="0 0 20 20" fill="currentColor">
9
+ <path fill-rule="evenodd" d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z" clip-rule="evenodd" />
10
+ </svg>
11
+ </button>
12
+ </div>
13
+ </div>
14
+ </div>
15
+ <div class="sidebar-content">
16
+ <% if prompts.any? %>
5
17
  <% 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>
18
+ <%= link_to workbench_index_path(prompt_id: prompt.id), class: "sidebar-item #{prompt == selected_prompt ? 'active' : ''}" do %>
19
+ <div class="flex items-center gap-2 min-w-0">
20
+ <% if prompt == selected_prompt %>
21
+ <span class="status-dot" style="background: var(--accent-500); margin-right: 0;"></span>
22
+ <% end %>
23
+ <span class="truncate"><%= prompt.name %></span>
24
+ </div>
25
+ <span class="sidebar-item-badge">v<%= prompt.version %></span>
9
26
  <% end %>
10
27
  <% end %>
11
- </div>
28
+ <% else %>
29
+ <div class="p-4 text-center">
30
+ <p class="text-sm text-muted">No prompts yet</p>
31
+ </div>
32
+ <% end %>
12
33
  </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">
34
+ <div class="sidebar-footer">
35
+ <%= link_to new_workbench_path, class: "btn btn-secondary btn-sm", style: "width: 100%;" do %>
36
+ <svg class="icon-sm" viewBox="0 0 20 20" fill="currentColor">
16
37
  <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
38
  </svg>
18
39
  <span>New Prompt</span>
19
40
  <% end %>
20
41
  </div>
21
- </div>
42
+ </div>
43
+
44
+ <script>
45
+ (() => {
46
+ const application = Stimulus.Application.start()
47
+
48
+ application.register("collapsible-sidebar", class extends Stimulus.Controller {
49
+ static targets = ["hideable"]
50
+ static values = { key: String }
51
+
52
+ connect() {
53
+ const isCollapsed = localStorage.getItem(this.keyValue) === 'true'
54
+ if (isCollapsed) {
55
+ this.element.classList.add('collapsed')
56
+ }
57
+ }
58
+
59
+ toggle() {
60
+ this.element.classList.toggle('collapsed')
61
+ const isCollapsed = this.element.classList.contains('collapsed')
62
+ localStorage.setItem(this.keyValue, isCollapsed)
63
+ }
64
+ })
65
+ })()
66
+ </script>