completion-kit 0.29.0 → 0.29.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +48 -3
  3. data/app/assets/javascripts/completion_kit/application.js +383 -0
  4. data/app/controllers/completion_kit/mcp_controller.rb +4 -0
  5. data/app/models/completion_kit/dataset.rb +53 -4
  6. data/app/models/completion_kit/metric.rb +1 -1
  7. data/app/models/completion_kit/prompt.rb +8 -0
  8. data/app/models/completion_kit/run.rb +1 -1
  9. data/app/services/completion_kit/api_config.rb +2 -1
  10. data/app/services/completion_kit/checks/regex.rb +12 -2
  11. data/app/services/completion_kit/csv_processor.rb +14 -2
  12. data/app/services/completion_kit/model_discovery_service.rb +2 -1
  13. data/app/services/completion_kit/ollama_client.rb +19 -2
  14. data/app/services/completion_kit/provider_endpoint.rb +10 -0
  15. data/app/views/completion_kit/api_reference/_authentication.html.erb +2 -2
  16. data/app/views/completion_kit/api_reference/_body.html.erb +0 -27
  17. data/app/views/completion_kit/api_reference/_example.html.erb +1 -1
  18. data/app/views/completion_kit/api_reference/_resource_card.html.erb +1 -1
  19. data/app/views/completion_kit/datasets/_form.html.erb +0 -28
  20. data/app/views/completion_kit/datasets/index.html.erb +1 -1
  21. data/app/views/completion_kit/metric_groups/_form.html.erb +1 -20
  22. data/app/views/completion_kit/metric_groups/index.html.erb +1 -1
  23. data/app/views/completion_kit/metrics/_starter_card.html.erb +1 -1
  24. data/app/views/completion_kit/metrics/index.html.erb +2 -2
  25. data/app/views/completion_kit/metrics/show.html.erb +4 -14
  26. data/app/views/completion_kit/prompts/_form.html.erb +1 -1
  27. data/app/views/completion_kit/prompts/index.html.erb +3 -3
  28. data/app/views/completion_kit/prompts/show.html.erb +8 -8
  29. data/app/views/completion_kit/provider_credentials/_models_card.html.erb +2 -2
  30. data/app/views/completion_kit/runs/_form.html.erb +3 -179
  31. data/app/views/completion_kit/runs/_response_row.html.erb +2 -3
  32. data/app/views/completion_kit/runs/_row.html.erb +3 -3
  33. data/app/views/completion_kit/runs/_run_config.html.erb +1 -1
  34. data/app/views/completion_kit/runs/show.html.erb +5 -19
  35. data/app/views/completion_kit/tags/index.html.erb +1 -1
  36. data/app/views/layouts/completion_kit/application.html.erb +2 -2
  37. data/lib/completion_kit/version.rb +1 -1
  38. data/lib/completion_kit.rb +14 -0
  39. data/lib/generators/completion_kit/templates/initializer.rb +18 -0
  40. metadata +1 -1
@@ -37,7 +37,18 @@ module CompletionKit
37
37
  def self.extract_variables(prompt)
38
38
  return [] if prompt.nil? || prompt.template.blank?
39
39
 
40
- prompt.template.scan(/\{\{([^}]+)\}\}/).flatten.map(&:strip).uniq
40
+ template = prompt.template
41
+ names = []
42
+ cursor = 0
43
+ while (opener = template.byteindex("{{", cursor)) && (closer = template.byteindex("}", opener + 2))
44
+ if closer > opener + 2 && template.byteslice(closer + 1, 1) == "}"
45
+ names << template.byteslice(opener + 2, closer - opener - 2)
46
+ cursor = closer + 2
47
+ else
48
+ cursor = closer + 1
49
+ end
50
+ end
51
+ names.map(&:strip).uniq
41
52
  end
42
53
 
43
54
  def self.validate_variables(run, headers)
@@ -57,7 +68,8 @@ module CompletionKit
57
68
  return prompt.template.dup if values.empty?
58
69
 
59
70
  names = values.keys.map { |name| Regexp.escape(name) }.join("|")
60
- prompt.template.gsub(/\{\{\s*(#{names})\s*\}\}/) { values[Regexp.last_match(1)].to_s }
71
+ pattern = Regexp.new("\\{\\{\\s*(#{names})\\s*\\}\\}", timeout: Checks::Regex.timeout)
72
+ prompt.template.gsub(pattern) { values[Regexp.last_match(1)].to_s }
61
73
  end
62
74
  end
63
75
  end
@@ -12,8 +12,9 @@ module CompletionKit
12
12
 
13
13
  def initialize(config:)
14
14
  @provider = config[:provider]
15
- @api_key = config[:api_key]
16
15
  @api_endpoint = config[:api_endpoint]
16
+ @api_key = config[:api_key]
17
+ @api_key = @api_key.presence || OllamaClient.fallback_api_key(@api_endpoint) if @provider == "ollama"
17
18
  @api_version = config[:api_version]
18
19
  @catalog_model_count = nil
19
20
  end
@@ -1,5 +1,22 @@
1
1
  module CompletionKit
2
2
  class OllamaClient < LlmClient
3
+ DEFAULT_ENDPOINT = "http://localhost:11434/v1"
4
+
5
+ def self.configured_endpoint
6
+ endpoint = CompletionKit.config.ollama_api_endpoint || ENV["OLLAMA_API_ENDPOINT"]
7
+ endpoint || (DEFAULT_ENDPOINT if CompletionKit.config.allow_loopback_endpoints)
8
+ end
9
+
10
+ def self.fallback_api_key(endpoint)
11
+ return unless ProviderEndpoint.same_destination?(root_url(endpoint), root_url(configured_endpoint))
12
+
13
+ env_key = ENV["OLLAMA_API_KEY"]
14
+ CompletionKit.config.tenant_scope ? env_key : CompletionKit.config.ollama_api_key || env_key
15
+ end
16
+
17
+ def self.root_url(endpoint)
18
+ endpoint.to_s.strip.delete_suffix("/").delete_suffix("/v1")
19
+ end
3
20
 
4
21
  def generate_completion(prompt, options = {})
5
22
  @temperature_dropped = false
@@ -75,12 +92,12 @@ module CompletionKit
75
92
  end
76
93
 
77
94
  def api_key
78
- @config[:api_key] || ENV["OLLAMA_API_KEY"]
95
+ @config[:api_key].presence || self.class.fallback_api_key(api_endpoint)
79
96
  end
80
97
 
81
98
  def api_endpoint
82
99
  raw = @config[:api_endpoint] || ENV["OLLAMA_API_ENDPOINT"]
83
- raw ||= "http://localhost:11434/v1" if CompletionKit.config.allow_loopback_endpoints
100
+ raw ||= DEFAULT_ENDPOINT if CompletionKit.config.allow_loopback_endpoints
84
101
  raw.to_s.delete_suffix("/")
85
102
  end
86
103
 
@@ -51,6 +51,16 @@ module CompletionKit
51
51
  nil
52
52
  end
53
53
 
54
+ def same_destination?(url, other)
55
+ destination = destination(url)
56
+ !destination.nil? && destination == destination(other)
57
+ end
58
+
59
+ def destination(url)
60
+ uri = parse(url)
61
+ [uri.scheme, uri.host.downcase, uri.port, uri.path.delete_suffix("/")] if uri
62
+ end
63
+
54
64
  def issues_for(addrs)
55
65
  return [:unresolvable] if addrs.empty?
56
66
  return [:unsafe_host] if addrs.any? { |ip| unsafe?(ip) }
@@ -1,8 +1,8 @@
1
1
  <% if token.present? %>
2
2
  <% masked = ck_masked_token(token) %>
3
3
  <div style="display: flex; align-items: center; gap: 0.4rem; margin: 0.5rem 0;">
4
- <button type="button" class="ck-chip ck-token-toggle" onclick="ckToggleToken(this)" data-masked="<%= masked %>" data-real="<%= token %>" aria-label="Reveal API token" aria-pressed="false" style="cursor: pointer;"><%= masked %></button>
5
- <button type="button" class="ck-icon-btn" title="Copy token" aria-label="Copy API token" onclick="var b=this;navigator.clipboard.writeText('<%= token %>').then(function(){b.classList.add('ck-api-copy--done');setTimeout(function(){b.classList.remove('ck-api-copy--done')},1500)})">
4
+ <button type="button" class="ck-chip ck-token-toggle" data-ck-token-toggle data-masked="<%= masked %>" data-real="<%= token %>" aria-label="Reveal API token" aria-pressed="false" style="cursor: pointer;"><%= masked %></button>
5
+ <button type="button" class="ck-icon-btn" title="Copy token" aria-label="Copy API token" data-ck-copy-text="<%= token %>" data-ck-copy-feedback>
6
6
  <%= heroicon_tag "clipboard-document", variant: :outline, size: 14, "aria-hidden": "true" %>
7
7
  </button>
8
8
  </div>
@@ -423,30 +423,3 @@
423
423
 
424
424
  </div>
425
425
  </div>
426
-
427
- <script>
428
- function ckToggleToken(el) {
429
- var showing = el.textContent === el.dataset.masked;
430
- el.textContent = showing ? el.dataset.real : el.dataset.masked;
431
- el.setAttribute('aria-pressed', showing ? 'true' : 'false');
432
- el.setAttribute('aria-label', showing ? 'Hide API token' : 'Reveal API token');
433
- navigator.clipboard.writeText(el.dataset.real).then(function() {
434
- el.classList.add('ck-api-copy--done');
435
- setTimeout(function() { el.classList.remove('ck-api-copy--done'); }, 1500);
436
- });
437
- }
438
-
439
- function ckCopyExample(btn) {
440
- var pre = btn.closest('.ck-api-example').querySelector('pre');
441
- var text = pre.textContent;
442
- var realToken = btn.dataset.realToken;
443
- var displayToken = btn.dataset.displayToken;
444
- if (realToken && displayToken) {
445
- text = text.split(displayToken).join(realToken);
446
- }
447
- navigator.clipboard.writeText(text).then(function() {
448
- btn.classList.add('ck-api-copy--done');
449
- setTimeout(function() { btn.classList.remove('ck-api-copy--done'); }, 1500);
450
- });
451
- }
452
- </script>
@@ -1,6 +1,6 @@
1
1
  <div class="ck-api-example">
2
2
  <pre class="ck-code ck-code--dark"><%= cmd %></pre>
3
- <button type="button" class="ck-api-copy" aria-label="Copy example to clipboard" onclick="ckCopyExample(this)" data-real-token="<%= real_token %>" data-display-token="<%= token %>">
3
+ <button type="button" class="ck-api-copy" aria-label="Copy example to clipboard" data-ck-copy-example data-real-token="<%= real_token %>" data-display-token="<%= token %>">
4
4
  <%= heroicon_tag "clipboard-document", variant: :outline, size: 14, "aria-hidden": "true" %>
5
5
  </button>
6
6
  </div>
@@ -19,6 +19,6 @@
19
19
  </div>
20
20
  <div class="ck-api-prompt-card__url">
21
21
  <code class="ck-endpoint__url" id="<%= dom_id %>"><%= url %></code>
22
- <button type="button" class="ck-icon-btn" title="Copy endpoint" aria-label="Copy endpoint URL" onclick="navigator.clipboard.writeText(document.getElementById('<%= dom_id %>').textContent)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="14" height="14" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
22
+ <button type="button" class="ck-icon-btn" title="Copy endpoint" aria-label="Copy endpoint URL" data-ck-copy="<%= dom_id %>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="14" height="14" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
23
23
  </div>
24
24
  </div>
@@ -46,34 +46,6 @@
46
46
  <%= form.submit(dataset.persisted? ? "Save dataset" : "Create dataset", class: ck_button_classes(:dark)) %>
47
47
  </div>
48
48
  </div>
49
-
50
- <script>
51
- function ckDatasetHeaderPreview() {
52
- var area = document.getElementById('dataset_csv_data');
53
- var box = document.getElementById('dataset-header-preview');
54
- var chips = document.getElementById('dataset-header-chips');
55
- if (!area || !box || !chips) return;
56
- var recognized = { expected_output: 'answer key', actual_output: 'pre-made output' };
57
- var firstLine = (area.value.split(/\r?\n/).find(function(l) { return l.trim() !== ''; }) || '');
58
- var headers = firstLine.split(',').map(function(h) { return h.trim().replace(/^"|"$/g, ''); }).filter(Boolean);
59
- if (headers.length === 0) { box.hidden = true; chips.textContent = ''; return; }
60
- box.hidden = false;
61
- chips.textContent = '';
62
- headers.forEach(function(h) {
63
- var role = recognized[h];
64
- var chip = document.createElement('span');
65
- chip.className = 'ck-header-chip' + (role ? ' ck-header-chip--special' : '');
66
- chip.textContent = role ? h + ' · ' + role : h;
67
- chips.appendChild(chip);
68
- });
69
- }
70
- document.addEventListener('turbo:load', ckDatasetHeaderPreview);
71
- (function() {
72
- var area = document.getElementById('dataset_csv_data');
73
- if (area) area.addEventListener('input', ckDatasetHeaderPreview);
74
- })();
75
- ckDatasetHeaderPreview();
76
- </script>
77
49
  <% end %>
78
50
 
79
51
  <% if dataset.persisted? %>
@@ -27,7 +27,7 @@
27
27
  </thead>
28
28
  <tbody>
29
29
  <% @datasets.each do |dataset| %>
30
- <tr onclick="window.location='<%= dataset_path(dataset) %>'" style="cursor: pointer;">
30
+ <tr data-ck-href="<%= dataset_path(dataset) %>" style="cursor: pointer;">
31
31
  <td>
32
32
  <%= link_to dataset_path(dataset), class: "ck-record-name" do %><strong><%= dataset.name %></strong><% end %>
33
33
  <% if dataset.tags.any? %>
@@ -47,8 +47,7 @@
47
47
  <button type="button"
48
48
  class="tag-mark tag-mark--off ck-metric-tag-filter__chip"
49
49
  style="--mark-color: var(--tag-<%= tag.color %>);"
50
- data-filter-tag="<%= tag.name %>"
51
- onclick="ckToggleMetricGroupFilter(this)"><%= tag.name %></button>
50
+ data-filter-tag="<%= tag.name %>"><%= tag.name %></button>
52
51
  <% end %>
53
52
  </div>
54
53
  <% end %>
@@ -78,24 +77,6 @@
78
77
  <%= hidden_field_tag "metric_group[metric_ids][]", "" %>
79
78
  </div>
80
79
 
81
- <script>
82
- function ckToggleMetricGroupFilter(btn) {
83
- btn.classList.toggle("tag-mark--off");
84
- var bar = btn.closest("[data-metric-tag-filter]");
85
- var grid = bar && bar.parentElement.querySelector("[data-metric-checkboxes]");
86
- if (!grid) return;
87
- var active = Array.prototype.map.call(
88
- bar.querySelectorAll(".ck-metric-tag-filter__chip:not(.tag-mark--off)"),
89
- function(b) { return b.getAttribute("data-filter-tag"); }
90
- );
91
- grid.querySelectorAll(".ck-checkbox-label").forEach(function(lbl) {
92
- var tags = (lbl.getAttribute("data-metric-tags") || "").split(",").filter(Boolean);
93
- var visible = active.length === 0 || tags.some(function(t) { return active.indexOf(t) !== -1; });
94
- lbl.style.display = visible ? "" : "none";
95
- });
96
- }
97
- </script>
98
-
99
80
  <div class="ck-actions">
100
81
  <% if metric_group.persisted? %>
101
82
  <%= ck_delete_trigger(form_id: metric_group_delete_id, label: "Delete metric group", confirm: metric_group_delete_confirm) %>
@@ -31,7 +31,7 @@
31
31
  </thead>
32
32
  <tbody>
33
33
  <% @metric_groups.each do |metric_group| %>
34
- <tr onclick="window.location='<%= metric_group_path(metric_group) %>'" style="cursor: pointer;">
34
+ <tr data-ck-href="<%= metric_group_path(metric_group) %>" style="cursor: pointer;">
35
35
  <td>
36
36
  <%= link_to metric_group_path(metric_group), class: "ck-record-name" do %><strong><%= metric_group.name %></strong><% end %>
37
37
  <% if metric_group.tags.any? %>
@@ -7,7 +7,7 @@
7
7
  <span class="ck-starter-card__cta">Preview →</span>
8
8
  <%= button_to "dismiss", dismiss_starter_metrics_path(key: starter.key),
9
9
  method: :post,
10
- form: { onclick: "event.stopPropagation();" },
10
+ form: { data: { ck_stop_propagation: true } },
11
11
  form_class: "inline-block ck-starter-card__dismiss-form",
12
12
  class: "ck-starter-card__dismiss",
13
13
  data: { turbo_confirm: "Hide \"#{starter.name}\" from this list?" } %>
@@ -28,7 +28,7 @@
28
28
  </thead>
29
29
  <tbody>
30
30
  <% @metrics.each do |metric| %>
31
- <tr onclick="window.location='<%= metric_path(metric) %>'" style="cursor: pointer;">
31
+ <tr data-ck-href="<%= metric_path(metric) %>" style="cursor: pointer;">
32
32
  <td>
33
33
  <%= link_to metric_path(metric), class: "ck-record-name" do %><strong><%= metric.name %></strong><% end %>
34
34
  <span class="ck-chip ck-chip--soft ck-metric-type-chip"><%= metric.check? ? "Check" : "Judge" %></span>
@@ -48,7 +48,7 @@
48
48
  <% if groups.any? %>
49
49
  <div class="ck-metrics-table__groups">
50
50
  <% groups.each do |g| %>
51
- <%= link_to metric_group_path(g), class: "ck-metric-group-pill ck-metric-group-pill--active", onclick: "event.stopPropagation();" do %>
51
+ <%= link_to metric_group_path(g), class: "ck-metric-group-pill ck-metric-group-pill--active" do %>
52
52
  <span class="ck-metric-group-pill__label"><%= g.name %></span>
53
53
  <% end %>
54
54
  <% end %>
@@ -91,7 +91,7 @@
91
91
  <% if v.draft? && vs.present? %>
92
92
  <span class="ck-version-score"><span class="ck-version-score__label">Match</span> <%= vs["after"] %>/<%= vs["tested"] %></span>
93
93
  <% end %>
94
- <button type="button" class="ck-cell-link ck-cell-link--delta" title="What changed from <%= pred.version_label %>" onclick="document.getElementById('ck-mvdiff-<%= v.id %>').showModal()">&Delta;</button>
94
+ <button type="button" class="ck-cell-link ck-cell-link--delta" title="What changed from <%= pred.version_label %>" data-ck-dialog-open="ck-mvdiff-<%= v.id %>">&Delta;</button>
95
95
  </div>
96
96
  <% end %>
97
97
  </div>
@@ -126,14 +126,14 @@
126
126
  <% pred = predecessor_of[v] %>
127
127
  <% next unless v.change_summary_against(pred) %>
128
128
  <% vs = v.validation_summary %>
129
- <dialog id="ck-mvdiff-<%= v.id %>" class="ck-modal" aria-labelledby="ck-mvdiff-title-<%= v.id %>" onclick="if(event.target===this)this.close()">
130
- <article class="ck-modal__panel" tabindex="-1" onclick="event.stopPropagation()">
129
+ <dialog id="ck-mvdiff-<%= v.id %>" class="ck-modal" aria-labelledby="ck-mvdiff-title-<%= v.id %>" data-ck-dialog<%= " data-ck-dialog-autoopen" if params[:show_change].to_i == v.id %>>
130
+ <article class="ck-modal__panel" tabindex="-1">
131
131
  <header class="ck-modal__header">
132
132
  <div class="ck-modal__heading">
133
133
  <h2 class="ck-modal__title" id="ck-mvdiff-title-<%= v.id %>"><%= pred.version_label %> &rarr; <%= v.version_label %></h2>
134
134
  <span class="ck-modal__meta">What changed in <%= v.version_label %><% if v.current? %> (live)<% elsif v.draft? %> (draft)<% end %></span>
135
135
  </div>
136
- <button type="button" class="ck-modal__close" aria-label="Close" onclick="this.closest('dialog').close()">&times;</button>
136
+ <button type="button" class="ck-modal__close" aria-label="Close" data-ck-dialog-close>&times;</button>
137
137
  </header>
138
138
  <div class="ck-modal__body">
139
139
  <% if v.draft? && vs.present? %>
@@ -223,13 +223,3 @@
223
223
  <% end %>
224
224
  </section>
225
225
  <% end %>
226
-
227
- <% if params[:show_change].present? %>
228
- <script>
229
- (function () {
230
- var dialog = document.getElementById("ck-mvdiff-<%= params[:show_change].to_i %>");
231
- if (dialog && typeof dialog.showModal === "function") dialog.showModal();
232
- })();
233
- </script>
234
- <% end %>
235
-
@@ -49,7 +49,7 @@
49
49
  <div class="ck-select-with-action">
50
50
  <%= form.select :llm_model, ck_grouped_models(available, prompt.llm_model, rejected: prompt.errors[:llm_model].any?), { include_blank: "Select a model" }, { class: "ck-input", id: "prompt_llm_model", **ck_field_aria(form, :llm_model) } %>
51
51
  <% ck_refreshing = CompletionKit::ProviderCredential.discovery_in_progress? %>
52
- <button type="button" class="ck-icon-btn<%= ' ck-icon-btn--spinning' if ck_refreshing %>" title="Refresh models" <%= 'disabled' if ck_refreshing %> onclick="ckRefreshModels()"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
52
+ <button type="button" class="ck-icon-btn<%= ' ck-icon-btn--spinning' if ck_refreshing %>" title="Refresh models" <%= 'disabled' if ck_refreshing %> data-ck-refresh-models><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="16" height="16"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
53
53
  </div>
54
54
  <% else %>
55
55
  <p class="ck-meta-copy">No models available. <%= link_to "Add a provider", provider_credentials_path, class: "ck-link" %> or click refresh after configuring a provider.</p>
@@ -28,15 +28,15 @@
28
28
  </thead>
29
29
  <tbody>
30
30
  <% @prompts.each do |prompt| %>
31
- <tr onclick="window.location='<%= prompt_path(prompt) %>'" style="cursor: pointer;">
31
+ <tr data-ck-href="<%= prompt_path(prompt) %>" style="cursor: pointer;">
32
32
  <td>
33
33
  <%= link_to prompt_path(prompt), class: "ck-record-name" do %><strong><%= prompt.name %></strong><% end %>
34
34
  <% if prompt.description.present? %>
35
35
  <p class="ck-prompts-table__desc"><%= truncate(prompt.description, length: 120) %></p>
36
36
  <% end %>
37
- <div class="ck-endpoint ck-endpoint--compact" onclick="event.stopPropagation()">
37
+ <div class="ck-endpoint ck-endpoint--compact" data-ck-row-ignore>
38
38
  <code class="ck-endpoint__url" id="prompt_endpoint_<%= prompt.id %>"><%= api_v1_prompt_path(prompt.slug) %></code>
39
- <button type="button" class="ck-icon-btn" title="Copy API path" onclick="event.stopPropagation();navigator.clipboard.writeText(document.getElementById('prompt_endpoint_<%= prompt.id %>').textContent)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
39
+ <button type="button" class="ck-icon-btn" title="Copy API path" data-ck-copy="prompt_endpoint_<%= prompt.id %>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
40
40
  </div>
41
41
  <% if prompt.tags.any? %>
42
42
  <div class="tag-marks-row">
@@ -16,7 +16,7 @@
16
16
  <% end %>
17
17
  <div class="ck-endpoint">
18
18
  <code class="ck-endpoint__url" id="prompt_endpoint"><%= request.base_url %><%= api_v1_prompt_path(@prompt.slug) %></code>
19
- <button type="button" class="ck-icon-btn" title="Copy endpoint" aria-label="Copy endpoint" onclick="navigator.clipboard.writeText(document.getElementById('prompt_endpoint').textContent)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="14" height="14" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
19
+ <button type="button" class="ck-icon-btn" title="Copy endpoint" aria-label="Copy endpoint" data-ck-copy="prompt_endpoint"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="14" height="14" aria-hidden="true"><path d="M0 6.75C0 5.784.784 5 1.75 5h1.5a.75.75 0 0 1 0 1.5h-1.5a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-1.5a.75.75 0 0 1 1.5 0v1.5A1.75 1.75 0 0 1 9.25 16h-7.5A1.75 1.75 0 0 1 0 14.25Z"/><path d="M5 1.75C5 .784 5.784 0 6.75 0h7.5C15.216 0 16 .784 16 1.75v7.5A1.75 1.75 0 0 1 14.25 11h-7.5A1.75 1.75 0 0 1 5 9.25Zm1.75-.25a.25.25 0 0 0-.25.25v7.5c0 .138.112.25.25.25h7.5a.25.25 0 0 0 .25-.25v-7.5a.25.25 0 0 0-.25-.25Z"/></svg></button>
20
20
  </div>
21
21
  <p class="ck-meta-copy" id="prompt_serves">
22
22
  <% if @serve_summary[:total].positive? %>
@@ -83,10 +83,10 @@
83
83
  <% best_score = scoped_runs.map(&:avg_score).compact.max %>
84
84
  <% best_pass_rate = scoped_runs.map(&:check_pass_rate).compact.max %>
85
85
  <% pred = predecessor_of[v] %>
86
- <tr class="<%= "ck-results-table__row--active" if v.id == @prompt.id %>" onclick="window.location='<%= prompt_path(v) %>'" style="cursor: pointer;">
86
+ <tr class="<%= "ck-results-table__row--active" if v.id == @prompt.id %>" data-ck-href="<%= prompt_path(v) %>" style="cursor: pointer;">
87
87
  <td>
88
88
  <div class="ck-version-cell">
89
- <div class="ck-version-cell__label" onclick="event.stopPropagation()">
89
+ <div class="ck-version-cell__label" data-ck-row-ignore>
90
90
  <%= link_to prompt_path(v), class: "ck-record-name" do %><strong>v<%= v.version_number %></strong><% end %>
91
91
  <% if v.current? %>
92
92
  <span class="ck-chip">Published</span>
@@ -95,7 +95,7 @@
95
95
  <% end %>
96
96
  </div>
97
97
  <% if version_changed.call(v, pred) %>
98
- <button type="button" class="ck-cell-link ck-cell-link--delta" title="What changed from v<%= pred.version_number %>" aria-label="What changed from v<%= pred.version_number %>" onclick="event.stopPropagation();document.getElementById('ck-vdiff-<%= v.id %>').showModal()"><span aria-hidden="true">&Delta;</span></button>
98
+ <button type="button" class="ck-cell-link ck-cell-link--delta" title="What changed from v<%= pred.version_number %>" aria-label="What changed from v<%= pred.version_number %>" data-ck-dialog-open="ck-vdiff-<%= v.id %>"><span aria-hidden="true">&Delta;</span></button>
99
99
  <% end %>
100
100
  </div>
101
101
  </td>
@@ -121,14 +121,14 @@
121
121
  <% versions.each do |v| %>
122
122
  <% pred = predecessor_of[v] %>
123
123
  <% next unless version_changed.call(v, pred) %>
124
- <dialog id="ck-vdiff-<%= v.id %>" class="ck-modal" aria-labelledby="ck-vdiff-title-<%= v.id %>" onclick="if(event.target===this)this.close()">
125
- <article class="ck-modal__panel" tabindex="-1" autofocus onclick="event.stopPropagation()">
124
+ <dialog id="ck-vdiff-<%= v.id %>" class="ck-modal" aria-labelledby="ck-vdiff-title-<%= v.id %>" data-ck-dialog>
125
+ <article class="ck-modal__panel" tabindex="-1" autofocus>
126
126
  <header class="ck-modal__header">
127
127
  <div class="ck-modal__heading">
128
128
  <h2 class="ck-modal__title" id="ck-vdiff-title-<%= v.id %>">v<%= pred.version_number %> <span aria-hidden="true">&rarr;</span> v<%= v.version_number %></h2>
129
129
  <span class="ck-modal__meta">What changed in <%= v.version_label %><% if v.current? %> (published)<% end %></span>
130
130
  </div>
131
- <button type="button" class="ck-modal__close" aria-label="Close" onclick="this.closest('dialog').close()">&times;</button>
131
+ <button type="button" class="ck-modal__close" aria-label="Close" data-ck-dialog-close>&times;</button>
132
132
  </header>
133
133
  <div class="ck-modal__body">
134
134
  <% if pred.llm_model != v.llm_model %>
@@ -188,7 +188,7 @@
188
188
  </thead>
189
189
  <tbody>
190
190
  <% suggestions.each do |s| %>
191
- <tr onclick="window.location='<%= suggestion_path(s, from: "prompt") %>'" style="cursor: pointer;">
191
+ <tr data-ck-href="<%= suggestion_path(s, from: "prompt") %>" style="cursor: pointer;">
192
192
  <td><%= link_to suggestion_path(s, from: "prompt"), class: "ck-record-name" do %><strong><%= s.run.name %></strong><% end %></td>
193
193
  <td><span class="ck-chip ck-chip--soft"><%= s.prompt.version_label %></span></td>
194
194
  <td class="ck-meta-copy"><%= truncate(ck_strip_markdown(s.reasoning), length: 100) %></td>
@@ -16,7 +16,7 @@
16
16
  <% if provider_credential.discovery_status == "completed" %>
17
17
  <span class="ck-model-list__summary-stamp">updated <time data-relative-time datetime="<%= provider_credential.updated_at.utc.iso8601 %>"><%= time_ago_in_words(provider_credential.updated_at) %> ago</time></span>
18
18
  <% end %>
19
- <button type="button" class="ck-icon-btn ck-model-list__refresh<%= ' ck-icon-btn--spinning' if discovering %>" title="Refresh models" aria-label="Refresh available models" <%= 'disabled' if discovering %> onclick="event.preventDefault();event.stopPropagation();fetch('<%= refresh_provider_credential_path(provider_credential) %>', {method:'POST',headers:{'X-CSRF-Token':document.querySelector('meta[name=csrf-token]').content}});if(window.ckStartDiscoveryPolling)ckStartDiscoveryPolling(8000)">
19
+ <button type="button" class="ck-icon-btn ck-model-list__refresh<%= ' ck-icon-btn--spinning' if discovering %>" title="Refresh models" aria-label="Refresh available models" <%= 'disabled' if discovering %> data-ck-provider-refresh="<%= refresh_provider_credential_path(provider_credential) %>">
20
20
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg>
21
21
  </button>
22
22
  </span>
@@ -74,7 +74,7 @@
74
74
  <div class="ck-model-list__summary">
75
75
  <span class="ck-model-list__summary-label">Available models <span class="ck-model-list__summary-count">0</span></span>
76
76
  <span class="ck-model-list__summary-meta">
77
- <button type="button" class="ck-icon-btn ck-model-list__refresh" title="Refresh models" aria-label="Refresh available models" onclick="event.preventDefault();fetch('<%= refresh_provider_credential_path(provider_credential) %>', {method:'POST',headers:{'X-CSRF-Token':document.querySelector('meta[name=csrf-token]').content}});if(window.ckStartDiscoveryPolling)ckStartDiscoveryPolling(8000)"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
77
+ <button type="button" class="ck-icon-btn ck-model-list__refresh" title="Refresh models" aria-label="Refresh available models" data-ck-provider-refresh="<%= refresh_provider_credential_path(provider_credential) %>"><svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" width="13" height="13" aria-hidden="true"><path fill-rule="evenodd" d="M13.836 2.477a.75.75 0 0 1 .75.75v3.182a.75.75 0 0 1-.75.75h-3.182a.75.75 0 0 1 0-1.5h1.37l-.84-.841a4.5 4.5 0 0 0-7.08.681.75.75 0 0 1-1.264-.808 6 6 0 0 1 9.44-.908l.84.84V3.227a.75.75 0 0 1 .75-.75Zm-.911 7.5A.75.75 0 0 1 13.199 11a6 6 0 0 1-9.44.908l-.84-.84v1.68a.75.75 0 0 1-1.5 0V9.567a.75.75 0 0 1 .75-.75h3.182a.75.75 0 0 1 0 1.5h-1.37l.84.841a4.5 4.5 0 0 0 7.08-.681.75.75 0 0 1 1.024-.274Z" clip-rule="evenodd"/></svg></button>
78
78
  </span>
79
79
  </div>
80
80
  <% end %>