completion-kit 0.20.4 → 0.21.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e49a3e185722be44be75a0e236862942841720af9f8b6bdcfe233778968d26ab
4
- data.tar.gz: 1545324b88bc8eef05f507d71a80e06804d4a9d3c65f99010b76610657ffa021
3
+ metadata.gz: 4fc454c51bfbe7917a7b3f248b055d58b6a147fe7e9325dadc196d9aebf7c762
4
+ data.tar.gz: 5e96ffb9c4d1e98c4917ec7847a6b734e9153142184d0b540585d8f7c8aba3b6
5
5
  SHA512:
6
- metadata.gz: 21f1b48c9ed2ba23b111eb1ff733ac62f180050ce3e1edd9ea31719ee25cf557025af86e00f13be27b6e04a79afb677ced23fa7e7e5a9580715249533559338c
7
- data.tar.gz: a7799c294e109c42a585f9fc7aa01e94d389291dd3fc1eec6821123ea4cb066e5cb805b803dfcf935f771b494bf1324fd7f5787f781746cfd0290d6c97b93641
6
+ metadata.gz: 11ad299c28fdb4dc4be3ee113c99ebf19ec6cb9d422dbc180cb253292adc7373d2d183aef7fd21c5e7293c1acc017f3c0689f34a5ded2e13b17674a86cacb6e6
7
+ data.tar.gz: b995b586a1b2c5422be141b9e94476ea0d32928dc81e8aca4210c54c1b4d8ea6cd0d7729621a0155438a2b7819977e6678e1000c343d9160ec09db7120be47e6
@@ -296,3 +296,75 @@ document.addEventListener("click", function(e) {
296
296
  btn.textContent = "Applied ✓";
297
297
  field.focus({ preventScroll: true });
298
298
  });
299
+
300
+ function ckSelectClose(sel) {
301
+ if (!sel.classList.contains("is-open")) return;
302
+ sel.classList.remove("is-open");
303
+ var menu = sel.querySelector("[data-ck-select-menu]");
304
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
305
+ if (menu) menu.hidden = true;
306
+ if (trigger) trigger.setAttribute("aria-expanded", "false");
307
+ }
308
+
309
+ function ckSelectOpen(sel) {
310
+ document.querySelectorAll("[data-ck-select].is-open").forEach(ckSelectClose);
311
+ sel.classList.add("is-open");
312
+ var menu = sel.querySelector("[data-ck-select-menu]");
313
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
314
+ if (menu) menu.hidden = false;
315
+ if (trigger) trigger.setAttribute("aria-expanded", "true");
316
+ var current = menu.querySelector('[aria-selected="true"]') || menu.querySelector('[role="option"]');
317
+ if (current) current.focus();
318
+ }
319
+
320
+ function ckSelectChoose(sel, option) {
321
+ var input = sel.querySelector("[data-ck-select-value]");
322
+ var label = sel.querySelector("[data-ck-select-label]");
323
+ var text = option.querySelector("[data-ck-select-text]");
324
+ sel.querySelectorAll('[role="option"]').forEach(function(o) {
325
+ o.setAttribute("aria-selected", o === option ? "true" : "false");
326
+ });
327
+ input.value = option.getAttribute("data-value");
328
+ if (text) label.textContent = text.textContent;
329
+ input.dispatchEvent(new Event("change", { bubbles: true }));
330
+ ckSelectClose(sel);
331
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
332
+ if (trigger) trigger.focus();
333
+ }
334
+
335
+ document.addEventListener("click", function(e) {
336
+ var trigger = e.target.closest("[data-ck-select-trigger]");
337
+ if (trigger) {
338
+ var sel = trigger.closest("[data-ck-select]");
339
+ if (sel.classList.contains("is-open")) { ckSelectClose(sel); } else { ckSelectOpen(sel); }
340
+ return;
341
+ }
342
+ var option = e.target.closest('[data-ck-select] [role="option"]');
343
+ if (option) {
344
+ ckSelectChoose(option.closest("[data-ck-select]"), option);
345
+ return;
346
+ }
347
+ document.querySelectorAll("[data-ck-select].is-open").forEach(function(sel) {
348
+ if (!sel.contains(e.target)) ckSelectClose(sel);
349
+ });
350
+ });
351
+
352
+ document.addEventListener("keydown", function(e) {
353
+ var sel = e.target.closest("[data-ck-select]");
354
+ if (!sel) return;
355
+ var trigger = sel.querySelector("[data-ck-select-trigger]");
356
+ var options = Array.prototype.slice.call(sel.querySelectorAll('[role="option"]'));
357
+ var open = sel.classList.contains("is-open");
358
+ if (e.target === trigger && !open) {
359
+ if (e.key === "ArrowDown" || e.key === "Enter" || e.key === " ") { e.preventDefault(); ckSelectOpen(sel); }
360
+ return;
361
+ }
362
+ if (!open) return;
363
+ var idx = options.indexOf(document.activeElement);
364
+ if (e.key === "ArrowDown") { e.preventDefault(); (options[idx + 1] || options[0]).focus(); }
365
+ else if (e.key === "ArrowUp") { e.preventDefault(); (options[idx - 1] || options[options.length - 1]).focus(); }
366
+ else if (e.key === "Home") { e.preventDefault(); options[0].focus(); }
367
+ else if (e.key === "End") { e.preventDefault(); options[options.length - 1].focus(); }
368
+ else if (e.key === "Enter" || e.key === " ") { e.preventDefault(); if (options[idx]) ckSelectChoose(sel, options[idx]); }
369
+ else if (e.key === "Escape") { e.preventDefault(); ckSelectClose(sel); trigger.focus(); }
370
+ });
@@ -1922,13 +1922,6 @@ label.ck-checkbox input {
1922
1922
  cursor: pointer;
1923
1923
  }
1924
1924
 
1925
- .ck-radio-info {
1926
- width: 16px;
1927
- height: 16px;
1928
- color: var(--ck-muted);
1929
- cursor: help;
1930
- }
1931
-
1932
1925
  .ck-field-row {
1933
1926
  display: flex;
1934
1927
  gap: 1rem;
@@ -1981,6 +1974,117 @@ label.ck-checkbox input {
1981
1974
  color: var(--ck-text);
1982
1975
  }
1983
1976
 
1977
+ .ck-select {
1978
+ position: relative;
1979
+ }
1980
+
1981
+ .ck-select__trigger {
1982
+ width: 100%;
1983
+ display: flex;
1984
+ align-items: center;
1985
+ justify-content: space-between;
1986
+ gap: 0.5rem;
1987
+ padding: 0.65rem 0.85rem;
1988
+ border: 1px solid var(--ck-line-strong);
1989
+ border-radius: var(--ck-radius);
1990
+ background: var(--ck-bg);
1991
+ color: var(--ck-text);
1992
+ font-family: var(--ck-mono);
1993
+ font-size: 0.9rem;
1994
+ text-align: left;
1995
+ cursor: pointer;
1996
+ transition: border-color 0.15s, box-shadow 0.15s;
1997
+ }
1998
+
1999
+ .ck-select__label {
2000
+ overflow: hidden;
2001
+ text-overflow: ellipsis;
2002
+ white-space: nowrap;
2003
+ }
2004
+
2005
+ .ck-select__trigger:focus-visible,
2006
+ .ck-select.is-open .ck-select__trigger {
2007
+ outline: none;
2008
+ border-color: var(--ck-accent);
2009
+ box-shadow: 0 0 0 3px var(--ck-accent-soft);
2010
+ }
2011
+
2012
+ .ck-select__chevron {
2013
+ display: inline-flex;
2014
+ flex-shrink: 0;
2015
+ color: var(--ck-muted);
2016
+ transition: transform 0.15s;
2017
+ }
2018
+
2019
+ .ck-select__chevron svg {
2020
+ width: 1rem;
2021
+ height: 1rem;
2022
+ }
2023
+
2024
+ .ck-select.is-open .ck-select__chevron {
2025
+ transform: rotate(180deg);
2026
+ }
2027
+
2028
+ .ck-select__menu {
2029
+ position: absolute;
2030
+ z-index: 30;
2031
+ left: 0;
2032
+ right: 0;
2033
+ top: calc(100% + 0.35rem);
2034
+ margin: 0;
2035
+ padding: 0.3rem;
2036
+ list-style: none;
2037
+ background: var(--ck-surface);
2038
+ border: 1px solid var(--ck-line-strong);
2039
+ border-radius: var(--ck-radius-lg);
2040
+ box-shadow: 0 16px 40px rgba(0, 0, 0, 0.45);
2041
+ max-height: 18rem;
2042
+ overflow-y: auto;
2043
+ font-family: var(--ck-mono);
2044
+ }
2045
+
2046
+ .ck-select__option {
2047
+ display: flex;
2048
+ align-items: center;
2049
+ gap: 0.5rem;
2050
+ padding: 0.5rem 0.6rem;
2051
+ border-radius: var(--ck-radius);
2052
+ color: var(--ck-text);
2053
+ font-size: 0.9rem;
2054
+ cursor: pointer;
2055
+ }
2056
+
2057
+ .ck-select__option:hover,
2058
+ .ck-select__option:focus-visible {
2059
+ outline: none;
2060
+ background: var(--ck-surface-hover);
2061
+ }
2062
+
2063
+ .ck-select__option[aria-selected="true"] {
2064
+ color: var(--ck-accent);
2065
+ }
2066
+
2067
+ .ck-select__tick {
2068
+ display: inline-flex;
2069
+ flex-shrink: 0;
2070
+ width: 1rem;
2071
+ color: var(--ck-accent);
2072
+ opacity: 0;
2073
+ }
2074
+
2075
+ .ck-select__tick svg {
2076
+ width: 1rem;
2077
+ height: 1rem;
2078
+ }
2079
+
2080
+ .ck-select__option[aria-selected="true"] .ck-select__tick {
2081
+ opacity: 1;
2082
+ }
2083
+
2084
+ .ck-select__text {
2085
+ flex: 1;
2086
+ }
2087
+
1984
2088
  .ck-section-title {
1985
2089
  font-size: 1.05rem;
1986
2090
  font-weight: 600;
@@ -169,6 +169,48 @@ module CompletionKit
169
169
  end
170
170
  end
171
171
 
172
+ CHECK_KIND_LABELS = {
173
+ "contains" => "Contains a phrase",
174
+ "not_contains" => "Does not contain a phrase",
175
+ "equals" => "Equals exactly",
176
+ "regex" => "Matches a pattern",
177
+ "valid_json" => "Is valid JSON",
178
+ "json_path_equals" => "A JSON field equals a value",
179
+ "length_bounds" => "Length is within a range",
180
+ "no_refusal" => "Is not a refusal"
181
+ }.freeze
182
+
183
+ CHECK_TARGET_LABELS = {
184
+ "response_text" => "The response text",
185
+ "input_data" => "The input row",
186
+ "json_path" => "A value from the response JSON"
187
+ }.freeze
188
+
189
+ CHECK_FIELD_LABELS = {
190
+ "value" => "Text to look for",
191
+ "pattern" => "Pattern",
192
+ "json_path" => "JSON path",
193
+ "expected" => "Expected value",
194
+ "target_path" => "Path into the JSON",
195
+ "min" => "Shortest allowed",
196
+ "max" => "Longest allowed",
197
+ "case_sensitive" => "Case sensitive",
198
+ "multiline" => "Multiline",
199
+ "trim" => "Trim whitespace"
200
+ }.freeze
201
+
202
+ def ck_check_kind_label(kind)
203
+ CHECK_KIND_LABELS.fetch(kind.to_s) { kind.to_s.humanize }
204
+ end
205
+
206
+ def ck_check_target_label(target)
207
+ CHECK_TARGET_LABELS.fetch(target.to_s) { target.to_s.humanize }
208
+ end
209
+
210
+ def ck_check_field_label(key)
211
+ CHECK_FIELD_LABELS.fetch(key.to_s) { key.to_s.humanize }
212
+ end
213
+
172
214
  def ck_result_change_badge(change)
173
215
  case change
174
216
  when "broke"
@@ -1,16 +1,16 @@
1
1
  <% config = local_assigns.fetch(:config) %>
2
2
  <dl class="ck-check-spec">
3
3
  <div class="ck-check-spec__row">
4
- <dt class="ck-check-spec__term">Kind</dt>
5
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["check_kind"] %></code></dd>
4
+ <dt class="ck-check-spec__term">Rule</dt>
5
+ <dd class="ck-check-spec__val"><%= ck_check_kind_label(config["check_kind"]) %></dd>
6
6
  </div>
7
7
  <div class="ck-check-spec__row">
8
- <dt class="ck-check-spec__term">Target</dt>
9
- <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= config["target"] %></code></dd>
8
+ <dt class="ck-check-spec__term">Checks</dt>
9
+ <dd class="ck-check-spec__val"><%= ck_check_target_label(config["target"]) %></dd>
10
10
  </div>
11
11
  <% config.except("check_kind", "target").each do |key, value| %>
12
12
  <div class="ck-check-spec__row">
13
- <dt class="ck-check-spec__term"><%= key.humanize %></dt>
13
+ <dt class="ck-check-spec__term"><%= ck_check_field_label(key) %></dt>
14
14
  <dd class="ck-check-spec__val"><code class="ck-check-spec__code"><%= value %></code></dd>
15
15
  </div>
16
16
  <% end %>
@@ -79,12 +79,10 @@
79
79
  <label class="ck-radio">
80
80
  <%= form.radio_button :metric_type, "llm_judge", checked: !metric.check? %>
81
81
  <span>LLM judge (1-5)</span>
82
- <%= heroicon_tag "information-circle", variant: :outline, size: 16, class: "ck-radio-info", "aria-hidden": "true", title: "An AI reads each response and rates it 1 to 5 stars against your rubric, with a written reason. Best for subjective quality: tone, helpfulness, accuracy." %>
83
82
  </label>
84
83
  <label class="ck-radio">
85
84
  <%= form.radio_button :metric_type, "check", checked: metric.check? %>
86
85
  <span>Deterministic check</span>
87
- <%= heroicon_tag "information-circle", variant: :outline, size: 16, class: "ck-radio-info", "aria-hidden": "true", title: "A rule that passes or fails instantly with no AI and no cost. Best for exact things: valid JSON, contains a phrase, no refusal." %>
88
86
  </label>
89
87
  </div>
90
88
  <% end %>
@@ -159,63 +157,62 @@
159
157
  <% check = metric.check_config || {} %>
160
158
  <div class="ck-field ck-field--spacious" data-ck-metric-editor="check" <%= "hidden" unless metric.check? %>>
161
159
  <p class="ck-section-title">Check</p>
162
- <p class="ck-hint">A deterministic pass/fail rule. Fill only the fields the chosen kind needs.</p>
160
+ <p class="ck-hint">A pass or fail rule with no AI and no cost. You only fill in the fields the chosen rule needs.</p>
163
161
 
164
162
  <div class="ck-field">
165
- <label class="ck-label" for="metric_check_kind">Check kind</label>
166
- <select name="metric[check_config][check_kind]" id="metric_check_kind" class="ck-input">
167
- <% CompletionKit::Checks::Registry.kinds.each do |kind| %>
168
- <option value="<%= kind %>"<%= " selected" if check["check_kind"] == kind %>><%= kind %></option>
169
- <% end %>
170
- </select>
163
+ <p class="ck-label" id="metric_check_kind_label">Rule</p>
164
+ <%= render "completion_kit/shared/branded_select",
165
+ name: "metric[check_config][check_kind]",
166
+ options: CompletionKit::Checks::Registry.kinds.map { |kind| [kind, ck_check_kind_label(kind)] },
167
+ selected: check["check_kind"] || CompletionKit::Checks::Registry.kinds.first,
168
+ labelledby: "metric_check_kind_label" %>
171
169
  </div>
172
170
 
173
171
  <div class="ck-field">
174
- <label class="ck-label" for="metric_check_target">Target</label>
175
- <select name="metric[check_config][target]" id="metric_check_target" class="ck-input">
176
- <% CompletionKit::Checks::TargetResolver::TARGETS.each do |target| %>
177
- <option value="<%= target %>"<%= " selected" if check["target"] == target %>><%= target %></option>
178
- <% end %>
179
- </select>
172
+ <p class="ck-label" id="metric_check_target_label">What to check</p>
173
+ <%= render "completion_kit/shared/branded_select",
174
+ name: "metric[check_config][target]",
175
+ options: CompletionKit::Checks::TargetResolver::TARGETS.map { |target| [target, ck_check_target_label(target)] },
176
+ selected: check["target"] || CompletionKit::Checks::TargetResolver::TARGETS.first,
177
+ labelledby: "metric_check_target_label" %>
180
178
  </div>
181
179
 
182
180
  <div class="ck-field" data-ck-check-field="target_path">
183
- <label class="ck-label" for="metric_check_target_path">Target path</label>
184
- <p class="ck-hint">Used when target is json_path, e.g. data.items.0.name.</p>
181
+ <label class="ck-label" for="metric_check_target_path">Path into the JSON</label>
182
+ <p class="ck-hint">Where to look inside the response, like data.items.0.name.</p>
185
183
  <input type="text" name="metric[check_config][target_path]" id="metric_check_target_path" class="ck-input" value="<%= check["target_path"] %>">
186
184
  </div>
187
185
 
188
186
  <div class="ck-field" data-ck-check-field="value">
189
- <label class="ck-label" for="metric_check_value">Value</label>
190
- <p class="ck-hint">The substring or exact string for contains, not_contains, or equals.</p>
187
+ <label class="ck-label" for="metric_check_value">Text to look for</label>
191
188
  <input type="text" name="metric[check_config][value]" id="metric_check_value" class="ck-input" value="<%= check["value"] %>">
192
189
  </div>
193
190
 
194
191
  <div class="ck-field" data-ck-check-field="pattern">
195
192
  <label class="ck-label" for="metric_check_pattern">Pattern</label>
196
- <p class="ck-hint">A regular expression for the regex kind.</p>
193
+ <p class="ck-hint">A regular expression to match against.</p>
197
194
  <input type="text" name="metric[check_config][pattern]" id="metric_check_pattern" class="ck-input" value="<%= check["pattern"] %>">
198
195
  </div>
199
196
 
200
197
  <div class="ck-field" data-ck-check-field="json_path">
201
198
  <label class="ck-label" for="metric_check_json_path">JSON path</label>
202
- <p class="ck-hint">Dotted path into parsed JSON for json_path_equals.</p>
199
+ <p class="ck-hint">The path to the value inside the response, like data.status.</p>
203
200
  <input type="text" name="metric[check_config][json_path]" id="metric_check_json_path" class="ck-input" value="<%= check["json_path"] %>">
204
201
  </div>
205
202
 
206
203
  <div class="ck-field" data-ck-check-field="expected">
207
- <label class="ck-label" for="metric_check_expected">Expected</label>
208
- <p class="ck-hint">The value the JSON path must equal.</p>
204
+ <label class="ck-label" for="metric_check_expected">Expected value</label>
205
+ <p class="ck-hint">The value it should equal.</p>
209
206
  <input type="text" name="metric[check_config][expected]" id="metric_check_expected" class="ck-input" value="<%= check["expected"] %>">
210
207
  </div>
211
208
 
212
209
  <div class="ck-field-row">
213
210
  <div class="ck-field" data-ck-check-field="min">
214
- <label class="ck-label" for="metric_check_min">Min length</label>
211
+ <label class="ck-label" for="metric_check_min">Shortest allowed</label>
215
212
  <input type="number" name="metric[check_config][min]" id="metric_check_min" class="ck-input" value="<%= check["min"] %>">
216
213
  </div>
217
214
  <div class="ck-field" data-ck-check-field="max">
218
- <label class="ck-label" for="metric_check_max">Max length</label>
215
+ <label class="ck-label" for="metric_check_max">Longest allowed</label>
219
216
  <input type="number" name="metric[check_config][max]" id="metric_check_max" class="ck-input" value="<%= check["max"] %>">
220
217
  </div>
221
218
  </div>
@@ -0,0 +1,18 @@
1
+ <% chosen = options.find { |value, _label| value.to_s == selected.to_s } || options.first %>
2
+ <div class="ck-select" data-ck-select>
3
+ <input type="hidden" name="<%= name %>" value="<%= chosen && chosen.first %>" data-ck-select-value>
4
+ <button type="button" class="ck-select__trigger" data-ck-select-trigger
5
+ aria-haspopup="listbox" aria-expanded="false" aria-labelledby="<%= labelledby %>">
6
+ <span class="ck-select__label" data-ck-select-label><%= chosen && chosen.last %></span>
7
+ <span class="ck-select__chevron" aria-hidden="true"><%= heroicon_tag "chevron-down" %></span>
8
+ </button>
9
+ <ul class="ck-select__menu" role="listbox" hidden data-ck-select-menu aria-labelledby="<%= labelledby %>">
10
+ <% options.each do |value, label| %>
11
+ <li class="ck-select__option" role="option" tabindex="-1" data-value="<%= value %>"
12
+ aria-selected="<%= chosen && chosen.first.to_s == value.to_s ? "true" : "false" %>">
13
+ <span class="ck-select__tick" aria-hidden="true"><%= heroicon_tag "check" %></span>
14
+ <span class="ck-select__text" data-ck-select-text><%= label %></span>
15
+ </li>
16
+ <% end %>
17
+ </ul>
18
+ </div>
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.20.4"
2
+ VERSION = "0.21.1"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: completion-kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.20.4
4
+ version: 0.21.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -419,6 +419,7 @@ files:
419
419
  - app/views/completion_kit/runs/index.html.erb
420
420
  - app/views/completion_kit/runs/new.html.erb
421
421
  - app/views/completion_kit/runs/show.html.erb
422
+ - app/views/completion_kit/shared/_branded_select.html.erb
422
423
  - app/views/completion_kit/shared/_settings_nav.html.erb
423
424
  - app/views/completion_kit/suggestions/_scoreboard.html.erb
424
425
  - app/views/completion_kit/suggestions/_state.html.erb