completion-kit 0.5.5 → 0.5.6

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: 89a2dd49e9edb75dada51386769cfd7e16f596c3b30c434588864487ab801d5b
4
- data.tar.gz: df8d9acfd4ef8aeab73508e7cf9adb2c083c7b9ef3d1c4304e1a3e01723b17e1
3
+ metadata.gz: bc297d90963b3ef6543ef3b2f14a94808c9a5a945047bd9ebb4b820fed1c9c9d
4
+ data.tar.gz: dc730069a2f27764f384068d2fab146e3cb402e24f3dfdeea33d700d50118d3a
5
5
  SHA512:
6
- metadata.gz: 9b03af38565cb43b7a57747896136817d954b06a89ba4551afb314ca93cd56a70e9d3068271d4f522741efa7d830b106a3a39e05997ea38fc3fb65996741ad08
7
- data.tar.gz: be0dad1d98c850854770351edbfdcd7867fbda7b6590634bae2997707a136a94cee066c79fd2ae8a259ea9c01db06a89b7088012a072655c7c627f7c475a801a
6
+ metadata.gz: c0fe08870fd298ca0ba0e5b850dc9992dbe8fc7cfebb81abb352d1543220c2925f195abd66113204f28ebfa2e48433d9ad389fc94f100ab1f8c9689edfdc6785
7
+ data.tar.gz: 1fc05c857f889ea0e461196471eca63d630c82a84d5447f870db9cd6bdfec9f539125d6189de27e3ee6cfc6969089105212f73e8a48485fecb8282ca886305a5
data/README.md CHANGED
@@ -142,7 +142,7 @@ Only one mode can be active.
142
142
 
143
143
  ## Concepts
144
144
 
145
- - **Prompt.** A versioned template with `{{variable}}` placeholders. Publishing freezes the template; editing a published prompt creates a new version.
145
+ - **Prompt.** A versioned template with `{{variable}}` placeholders. Editing a prompt that's already been run creates a new version, so earlier results stay reproducible.
146
146
  - **Dataset.** A CSV of real inputs. Each row becomes one test case.
147
147
  - **Run.** One execution of a prompt against a dataset. Captures every input (model, temperature, metrics) and stores all outputs and scores.
148
148
  - **Response.** The model's output for one dataset row, with reviews attached.
@@ -105,11 +105,16 @@ document.addEventListener("mouseout", function(e) {
105
105
  });
106
106
 
107
107
  var ckRefreshing = false;
108
+ function ckSetRefreshButtonsBusy(busy) {
109
+ document.querySelectorAll('.ck-icon-btn[title="Refresh models"]').forEach(function(btn) {
110
+ btn.classList.toggle('ck-icon-btn--spinning', busy);
111
+ btn.disabled = busy;
112
+ });
113
+ }
108
114
  function ckRefreshModels() {
109
115
  if (ckRefreshing) return;
110
116
  ckRefreshing = true;
111
- var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
112
- if (btn) btn.classList.add('ck-icon-btn--spinning');
117
+ ckSetRefreshButtonsBusy(true);
113
118
  ckUpdateRefreshProgress();
114
119
  var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
115
120
  fetch("/completion_kit/refresh_models", {
@@ -149,8 +154,7 @@ document.addEventListener("turbo:before-stream-render", function(event) {
149
154
  }
150
155
  if (target === "prompt_llm_model" || target === "run_judge_model") {
151
156
  ckRefreshing = false;
152
- var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
153
- if (btn) btn.classList.remove('ck-icon-btn--spinning');
157
+ ckSetRefreshButtonsBusy(false);
154
158
  var status = document.getElementById('refresh-status');
155
159
  if (status) { status.textContent = 'Models updated.'; setTimeout(function() { status.textContent = ' '; }, 3000); }
156
160
  }
@@ -1164,6 +1164,17 @@ tr:hover .ck-chip--publish {
1164
1164
  cursor: help;
1165
1165
  }
1166
1166
 
1167
+ .ck-model-table tr.ck-model-table__section td {
1168
+ padding: 0.85rem 0 0.3rem;
1169
+ font-family: var(--ck-mono);
1170
+ font-size: 0.66rem;
1171
+ font-weight: 600;
1172
+ letter-spacing: 0.16em;
1173
+ text-transform: uppercase;
1174
+ color: var(--ck-dim);
1175
+ }
1176
+ .ck-model-table tr.ck-model-table__section:first-child td { padding-top: 0; }
1177
+
1167
1178
  .ck-model-list__summary {
1168
1179
  display: flex;
1169
1180
  align-items: center;
@@ -1278,6 +1289,102 @@ tr:hover .ck-chip--publish {
1278
1289
  max-width: 36rem;
1279
1290
  }
1280
1291
 
1292
+ .ck-version-note {
1293
+ margin: 0 0 1rem;
1294
+ max-width: 36rem;
1295
+ font-family: var(--ck-sans);
1296
+ font-size: 0.85rem;
1297
+ line-height: 1.5;
1298
+ color: var(--ck-muted);
1299
+ }
1300
+
1301
+ .ck-version-change {
1302
+ margin: 0.5rem 0 0;
1303
+ display: flex;
1304
+ align-items: center;
1305
+ gap: 0.5rem;
1306
+ flex-wrap: wrap;
1307
+ font-family: var(--ck-mono);
1308
+ font-size: 0.8rem;
1309
+ }
1310
+ .ck-version-change__label {
1311
+ font-size: 0.66rem;
1312
+ font-weight: 600;
1313
+ letter-spacing: 0.12em;
1314
+ text-transform: uppercase;
1315
+ color: var(--ck-dim);
1316
+ }
1317
+ .ck-version-change__old {
1318
+ padding: 0.15em 0.5em;
1319
+ border-radius: var(--ck-radius);
1320
+ background: var(--ck-danger-soft);
1321
+ color: var(--ck-danger);
1322
+ text-decoration: line-through;
1323
+ }
1324
+ .ck-version-change__arrow { color: var(--ck-dim); }
1325
+ .ck-version-change__new {
1326
+ padding: 0.15em 0.5em;
1327
+ border-radius: var(--ck-radius);
1328
+ background: var(--ck-success-soft);
1329
+ color: var(--ck-success);
1330
+ }
1331
+
1332
+ .ck-modal__body .ck-version-change + .ck-suggest-diff { margin-top: 0.9rem; }
1333
+
1334
+ .ck-cell-link {
1335
+ background: none;
1336
+ border: none;
1337
+ padding: 0;
1338
+ font-family: var(--ck-mono);
1339
+ font-size: 0.8rem;
1340
+ color: var(--ck-accent);
1341
+ cursor: pointer;
1342
+ text-decoration: underline;
1343
+ text-underline-offset: 2px;
1344
+ }
1345
+ .ck-cell-link:hover { color: var(--ck-accent-hover); }
1346
+ .ck-cell-link--delta {
1347
+ font-size: 1rem;
1348
+ font-weight: 700;
1349
+ text-decoration: none;
1350
+ color: var(--ck-dim);
1351
+ }
1352
+ .ck-cell-link--delta:hover { color: var(--ck-accent); }
1353
+
1354
+ /* Version-history table: cell that pairs the version label + publish control (left) with the diff trigger (right) */
1355
+ .ck-version-cell {
1356
+ display: flex;
1357
+ align-items: center;
1358
+ justify-content: space-between;
1359
+ gap: 0.75rem;
1360
+ }
1361
+ .ck-version-cell__label { display: flex; align-items: center; gap: 0.5rem; }
1362
+
1363
+ /* the version currently being viewed: faint accent wash (like the model chips)
1364
+ plus a bright accent border drawn on the cells (Safari can't box-shadow a <tr>) */
1365
+ .ck-results-table tbody tr.ck-results-table__row--active,
1366
+ .ck-results-table tbody tr.ck-results-table__row--active:hover {
1367
+ background: var(--ck-accent-soft);
1368
+ }
1369
+ .ck-results-table tbody tr.ck-results-table__row--active td {
1370
+ box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent);
1371
+ }
1372
+ .ck-results-table tbody tr.ck-results-table__row--active td:first-child {
1373
+ box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent), inset 2px 0 0 var(--ck-accent);
1374
+ }
1375
+ .ck-results-table tbody tr.ck-results-table__row--active td:last-child {
1376
+ box-shadow: inset 0 2px 0 var(--ck-accent), inset 0 -2px 0 var(--ck-accent), inset -2px 0 0 var(--ck-accent);
1377
+ }
1378
+ /* when it's the last row, round its bottom corners so the border isn't clipped
1379
+ by the table's rounded container */
1380
+ .ck-results-table tbody tr.ck-results-table__row--active:last-child td:first-child {
1381
+ border-bottom-left-radius: calc(var(--ck-radius-lg) - 1px);
1382
+ }
1383
+ .ck-results-table tbody tr.ck-results-table__row--active:last-child td:last-child {
1384
+ border-bottom-right-radius: calc(var(--ck-radius-lg) - 1px);
1385
+ }
1386
+ .ck-results-table tbody tr.ck-results-table__row--active strong { color: var(--ck-accent-hover); }
1387
+
1281
1388
  .ck-field-hint {
1282
1389
  font-family: var(--ck-sans);
1283
1390
  font-size: 0.8rem;
@@ -1659,30 +1766,54 @@ tr:hover .ck-chip--publish {
1659
1766
  color: var(--ck-text);
1660
1767
  }
1661
1768
 
1769
+ @keyframes ck-flash-in {
1770
+ from { opacity: 0; transform: translateY(-4px); }
1771
+ to { opacity: 1; transform: translateY(0); }
1772
+ }
1773
+
1662
1774
  .ck-flash {
1663
- margin-bottom: 1rem;
1664
- padding: 0.75rem 1rem;
1665
- border-radius: var(--ck-radius);
1775
+ margin-bottom: 1.25rem;
1776
+ padding: 0.8rem 1rem;
1666
1777
  border: 1px solid transparent;
1778
+ border-radius: var(--ck-radius);
1667
1779
  font-family: var(--ck-sans);
1668
- font-size: 0.95rem;
1780
+ font-size: 0.9rem;
1781
+ line-height: 1.6;
1782
+ animation: ck-flash-in 0.22s ease both;
1669
1783
  }
1784
+ .ck-flash--notice { background: rgba(45, 212, 168, 0.08); border-color: rgba(45, 212, 168, 0.32); }
1785
+ .ck-flash--alert { background: rgba(248, 113, 113, 0.08); border-color: rgba(248, 113, 113, 0.32); }
1670
1786
 
1671
- .ck-flash--notice {
1672
- border-color: rgba(34, 197, 94, 0.3);
1673
- background: var(--ck-success-soft);
1674
- color: var(--ck-success);
1787
+ /* layout flash: a mono status tag (the engine's kicker treatment) inline before the message */
1788
+ .ck-flash__label {
1789
+ margin-right: 0.6rem;
1790
+ font-family: var(--ck-mono);
1791
+ font-size: 0.7rem;
1792
+ font-weight: 600;
1793
+ letter-spacing: 0.14em;
1794
+ text-transform: uppercase;
1795
+ white-space: nowrap;
1675
1796
  }
1676
-
1677
- .ck-flash--alert {
1678
- border-color: rgba(239, 68, 68, 0.3);
1679
- background: var(--ck-danger-soft);
1680
- color: var(--ck-danger);
1797
+ .ck-flash__label::before {
1798
+ content: "";
1799
+ display: inline-block;
1800
+ width: 6px;
1801
+ height: 6px;
1802
+ margin-right: 0.45rem;
1803
+ border-radius: 50%;
1804
+ background: currentColor;
1805
+ box-shadow: 0 0 7px currentColor;
1806
+ vertical-align: 0.12em;
1681
1807
  }
1808
+ .ck-flash--notice .ck-flash__label { color: var(--ck-success); }
1809
+ .ck-flash--alert .ck-flash__label { color: var(--ck-danger); }
1810
+ .ck-flash__body { color: var(--ck-text); }
1682
1811
 
1683
- .ck-flash__title {
1684
- margin: 0;
1685
- font-weight: 700;
1812
+ /* form-error summary inside .ck-flash--alert: title sentence + ck-error-list */
1813
+ .ck-flash__title { margin: 0; font-weight: 600; color: var(--ck-danger); }
1814
+
1815
+ @media (prefers-reduced-motion: reduce) {
1816
+ .ck-flash { animation: none; }
1686
1817
  }
1687
1818
 
1688
1819
  .ck-banner {
@@ -2012,6 +2143,10 @@ select.ck-input {
2012
2143
  box-shadow: 0 30px 80px rgba(0, 0, 0, 0.55);
2013
2144
  overflow: hidden;
2014
2145
  }
2146
+ /* the panel is the dialog's autofocus target (so the close button isn't auto-focused);
2147
+ it's a container, not a control, so don't show a ring on it */
2148
+ .ck-modal__panel:focus,
2149
+ .ck-modal__panel:focus-visible { outline: none; }
2015
2150
 
2016
2151
  .ck-modal__header {
2017
2152
  display: flex;
@@ -2048,9 +2183,8 @@ select.ck-input {
2048
2183
 
2049
2184
  .ck-modal__close {
2050
2185
  appearance: none;
2186
+ position: relative;
2051
2187
  display: inline-flex;
2052
- align-items: center;
2053
- justify-content: center;
2054
2188
  width: 28px;
2055
2189
  height: 28px;
2056
2190
  padding: 0;
@@ -2059,12 +2193,25 @@ select.ck-input {
2059
2193
  outline: none;
2060
2194
  background: transparent;
2061
2195
  color: var(--ck-dim);
2062
- font-family: var(--ck-sans);
2063
- font-size: 1.5rem;
2064
- line-height: 1;
2196
+ font-size: 0;
2065
2197
  cursor: pointer;
2066
2198
  transition: color 0.15s, background 0.15s;
2067
2199
  }
2200
+ /* the "×" is drawn as two crossed bars centred on the button, so it (and the
2201
+ focus ring around the button) sit dead centre regardless of font metrics */
2202
+ .ck-modal__close::before,
2203
+ .ck-modal__close::after {
2204
+ content: "";
2205
+ position: absolute;
2206
+ top: 50%;
2207
+ left: 50%;
2208
+ width: 13px;
2209
+ height: 1.6px;
2210
+ border-radius: 1px;
2211
+ background: currentColor;
2212
+ }
2213
+ .ck-modal__close::before { transform: translate(-50%, -50%) rotate(45deg); }
2214
+ .ck-modal__close::after { transform: translate(-50%, -50%) rotate(-45deg); }
2068
2215
 
2069
2216
  .ck-modal__close:hover {
2070
2217
  color: var(--ck-text);
@@ -2083,6 +2230,8 @@ select.ck-input {
2083
2230
  overflow: auto;
2084
2231
  padding: 0 1.5rem;
2085
2232
  }
2233
+ /* no footer? then the body needs its own bottom padding */
2234
+ .ck-modal__body:last-child { padding-bottom: 1.5rem; }
2086
2235
 
2087
2236
  .ck-modal__footer {
2088
2237
  display: flex;
@@ -2878,6 +3027,29 @@ select.ck-input {
2878
3027
  color: var(--ck-dim);
2879
3028
  }
2880
3029
 
3030
+ .ck-prompts-table__desc {
3031
+ margin: 0.3rem 0 0;
3032
+ font-family: var(--ck-sans);
3033
+ font-size: 0.82rem;
3034
+ font-weight: 400;
3035
+ line-height: 1.45;
3036
+ color: var(--ck-muted);
3037
+ white-space: normal;
3038
+ max-width: 42ch;
3039
+ }
3040
+
3041
+ .ck-endpoint--compact {
3042
+ margin-top: 0.45rem;
3043
+ max-width: 26rem;
3044
+ }
3045
+ .ck-endpoint--compact .ck-endpoint__url {
3046
+ min-width: 0;
3047
+ white-space: nowrap;
3048
+ overflow: hidden;
3049
+ text-overflow: ellipsis;
3050
+ }
3051
+ .ck-endpoint--compact .ck-icon-btn { flex-shrink: 0; }
3052
+
2881
3053
  .ck-clamp-2 {
2882
3054
  display: -webkit-box;
2883
3055
  -webkit-line-clamp: 2;
@@ -50,7 +50,7 @@ module CompletionKit
50
50
 
51
51
  def publish
52
52
  @prompt.publish!
53
- redirect_to prompt_path(@prompt), notice: "#{@prompt.display_name} is now the current version."
53
+ redirect_to prompt_path(@prompt), notice: "#{@prompt.display_name} is now the published version."
54
54
  end
55
55
 
56
56
  private
@@ -72,6 +72,41 @@ module CompletionKit
72
72
  CompletionKit::ProviderCredential::PROVIDER_LABELS[provider.to_s] || provider.to_s.titleize
73
73
  end
74
74
 
75
+ OPENAI_MODEL_FAMILY_ORDER = ["GPT-5", "GPT-4", "o-series", "GPT-3.5", "GPT-OSS", "Other"].freeze
76
+
77
+ def ck_openai_model_family(model_id)
78
+ id = model_id.to_s
79
+ return "GPT-5" if id.match?(/\Agpt-5/i)
80
+ return "GPT-4" if id.match?(/\Agpt-4/i)
81
+ return "GPT-3.5" if id.match?(/\Agpt-3/i)
82
+ return "GPT-OSS" if id.match?(/\Agpt-oss/i)
83
+ return "o-series" if id.match?(/\Ao\d/i)
84
+ "Other"
85
+ end
86
+
87
+ # Groups a provider's models for the models-card table, mirroring how the
88
+ # dropdown sub-groups: OpenRouter clusters by upstream vendor (the part
89
+ # before "/"); OpenAI clusters by family; everyone else stays flat. Returns
90
+ # [[section_label_or_nil, [models]], ...]. A single section collapses to a
91
+ # nil label so we don't render a redundant header.
92
+ def ck_model_table_sections(models)
93
+ models = models.to_a
94
+ sections =
95
+ case models.first&.provider
96
+ when "openrouter"
97
+ models.group_by { |m| m.model_id.to_s.split("/", 2).first.delete_prefix("~") }
98
+ .sort_by { |label, _| label }
99
+ when "openai"
100
+ grouped = models.group_by { |m| ck_openai_model_family(m.model_id) }
101
+ ordered = OPENAI_MODEL_FAMILY_ORDER.filter_map { |label| [label, grouped[label]] if grouped[label] }
102
+ extras = (grouped.keys - OPENAI_MODEL_FAMILY_ORDER).sort.map { |label| [label, grouped[label]] }
103
+ ordered + extras
104
+ else
105
+ [[nil, models]]
106
+ end
107
+ sections.size <= 1 ? [[nil, models]] : sections
108
+ end
109
+
75
110
  def ck_model_option_label(model)
76
111
  return "#{model[:name]} (?)" if model.key?(:judging_confirmed) && !model[:judging_confirmed]
77
112
  model[:name]
@@ -85,20 +120,33 @@ module CompletionKit
85
120
  end
86
121
  end
87
122
 
88
- groups = models.group_by do |m|
89
- if m[:provider] == "openrouter"
90
- upstream = m[:id].to_s.split("/", 2).first
91
- "OpenRouter — #{upstream}"
92
- else
93
- ck_provider_label(m[:provider])
94
- end
95
- end
96
-
97
- ordered_keys = groups.keys.sort_by { |label| [label.start_with?("OpenRouter") ? 1 : 0, label] }
123
+ groups = models.group_by { |m| ck_model_optgroup_label(m) }
124
+ ordered_keys = groups.keys.sort_by { |label| ck_model_optgroup_sort_key(label) }
98
125
  grouped = ordered_keys.map { |label| [label, groups[label].map { |m| [ck_model_option_label(m), m[:id]] }] }
99
126
  grouped_options_for_select(grouped, selected)
100
127
  end
101
128
 
129
+ # Optgroup label for the model select — mirrors the provider models table:
130
+ # OpenRouter splits by upstream vendor, OpenAI splits by family, everyone
131
+ # else is a single group.
132
+ def ck_model_optgroup_label(model)
133
+ case model[:provider]
134
+ when "openrouter" then "OpenRouter — #{model[:id].to_s.split("/", 2).first.delete_prefix("~")}"
135
+ when "openai" then "OpenAI — #{ck_openai_model_family(model[:id])}"
136
+ else ck_provider_label(model[:provider])
137
+ end
138
+ end
139
+
140
+ def ck_model_optgroup_sort_key(label)
141
+ if label.start_with?("OpenAI — ")
142
+ [0, OPENAI_MODEL_FAMILY_ORDER.index(label.delete_prefix("OpenAI — ")), label]
143
+ elsif label.start_with?("OpenRouter")
144
+ [2, 0, label]
145
+ else
146
+ [1, 0, label]
147
+ end
148
+ end
149
+
102
150
  def ck_model_options_html(scope)
103
151
  models = CompletionKit::ApiConfig.available_models(scope: scope)
104
152
  return "" if models.empty?
@@ -47,6 +47,14 @@ module CompletionKit
47
47
  false
48
48
  end
49
49
 
50
+ def model_count
51
+ Model.where(provider: provider).active.count
52
+ end
53
+
54
+ def self.discovery_in_progress?
55
+ where(discovery_status: "discovering").exists?
56
+ end
57
+
50
58
  def prompt_count
51
59
  model_ids = Model.where(provider: provider).pluck(:model_id)
52
60
  return 0 if model_ids.empty?
@@ -10,6 +10,10 @@
10
10
  </div>
11
11
  <% end %>
12
12
 
13
+ <% if prompt.persisted? && prompt.runs.exists? %>
14
+ <p class="ck-version-note">Editing <%= prompt.version_label %> — it has runs, so saving creates a new version of this prompt.</p>
15
+ <% end %>
16
+
13
17
  <div class="ck-card ck-form-card">
14
18
  <div class="ck-field">
15
19
  <%= form.label :name, "Name", class: "ck-label" %>
@@ -33,7 +37,8 @@
33
37
  <% if available.any? %>
34
38
  <div class="ck-select-with-action">
35
39
  <%= form.select :llm_model, ck_grouped_models(available, prompt.llm_model), { include_blank: "— Select a model —" }, { class: "ck-input", id: "prompt_llm_model" } %>
36
- <button type="button" class="ck-icon-btn" title="Refresh models" 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>
40
+ <% ck_refreshing = CompletionKit::ProviderCredential.discovery_in_progress? %>
41
+ <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>
37
42
  </div>
38
43
  <% else %>
39
44
  <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>
@@ -30,6 +30,13 @@
30
30
  <tr onclick="window.location='<%= prompt_path(prompt) %>'" style="cursor: pointer;">
31
31
  <td>
32
32
  <strong><%= prompt.name %></strong>
33
+ <% if prompt.description.present? %>
34
+ <p class="ck-prompts-table__desc"><%= truncate(prompt.description, length: 120) %></p>
35
+ <% end %>
36
+ <div class="ck-endpoint ck-endpoint--compact" onclick="event.stopPropagation()">
37
+ <code class="ck-endpoint__url" id="prompt_endpoint_<%= prompt.id %>"><%= api_v1_prompt_path(prompt.slug) %></code>
38
+ <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
+ </div>
33
40
  <% if prompt.tags.any? %>
34
41
  <div class="tag-marks-row">
35
42
  <%= render "completion_kit/tags/marks", tags: prompt.tags %>
@@ -7,6 +7,7 @@
7
7
  <div>
8
8
  <div class="ck-inline">
9
9
  <h1 class="ck-title"><%= @prompt.name %></h1>
10
+ <span class="ck-chip ck-chip--soft"><%= @prompt.version_label %></span>
10
11
  <span class="ck-chip"><%= @prompt.llm_model %></span>
11
12
  </div>
12
13
  <% if @prompt.description.present? %>
@@ -47,7 +48,9 @@
47
48
  <pre class="ck-code ck-code--dark"><%= @prompt.template %></pre>
48
49
  </section>
49
50
 
50
- <% versions = @prompt.family_versions %>
51
+ <% versions = @prompt.family_versions.includes(runs: { responses: :reviews }).to_a %>
52
+ <% predecessor_of = versions.index_with { |v| versions.detect { |o| o.version_number < v.version_number } } %>
53
+ <% version_changed = ->(v, pred) { pred && (pred.template != v.template || pred.llm_model != v.llm_model) } %>
51
54
  <% if versions.size > 1 %>
52
55
  <section class="ck-card--spaced">
53
56
  <p class="ck-kicker">Versions</p>
@@ -56,28 +59,82 @@
56
59
  <tr>
57
60
  <th>Version</th>
58
61
  <th>Model</th>
62
+ <th>Best score</th>
59
63
  <th>Created</th>
60
- <th></th>
61
64
  </tr>
62
65
  </thead>
63
66
  <tbody>
64
67
  <% versions.each do |v| %>
65
- <tr>
66
- <td><strong>v<%= v.version_number %></strong></td>
68
+ <% best_score = v.runs.map(&:avg_score).compact.max %>
69
+ <% pred = predecessor_of[v] %>
70
+ <tr class="<%= "ck-results-table__row--active" if v.id == @prompt.id %>" onclick="window.location='<%= prompt_path(v) %>'" style="cursor: pointer;">
71
+ <td>
72
+ <div class="ck-version-cell">
73
+ <div class="ck-version-cell__label" onclick="event.stopPropagation()">
74
+ <strong>v<%= v.version_number %></strong>
75
+ <% if v.current? %>
76
+ <span class="ck-chip">Published</span>
77
+ <% else %>
78
+ <%= button_to "Publish", publish_prompt_path(v), method: :post, class: "ck-chip ck-chip--publish", form_class: "inline-block" %>
79
+ <% end %>
80
+ </div>
81
+ <% if version_changed.call(v, pred) %>
82
+ <button type="button" class="ck-cell-link ck-cell-link--delta" title="What changed from v<%= pred.version_number %>" onclick="event.stopPropagation();document.getElementById('ck-vdiff-<%= v.id %>').showModal()">&Delta;</button>
83
+ <% end %>
84
+ </div>
85
+ </td>
67
86
  <td><span class="ck-chip ck-chip--soft"><%= v.llm_model %></span></td>
68
- <td class="ck-meta-copy"><time datetime="<%= v.created_at.iso8601 %>" data-local-time><%= v.created_at.utc.strftime("%b %-d, %Y at %-I:%M %p UTC") %></time></td>
69
87
  <td>
70
- <% if v.current? %>
71
- <span class="ck-chip">Current</span>
88
+ <% if best_score %>
89
+ <span class="<%= ck_badge_classes(ck_score_kind(best_score)) %>"><%= best_score %></span>
72
90
  <% else %>
73
- <%= button_to "Publish", publish_prompt_path(v), method: :post, class: "ck-chip ck-chip--publish", form_class: "inline-block" %>
91
+ <span class="ck-prompts-table__dim">—</span>
74
92
  <% end %>
75
93
  </td>
94
+ <td class="ck-meta-copy"><time datetime="<%= v.created_at.iso8601 %>" data-local-time><%= v.created_at.utc.strftime("%b %-d, %Y at %-I:%M %p UTC") %></time></td>
76
95
  </tr>
77
96
  <% end %>
78
97
  </tbody>
79
98
  </table>
80
99
  </section>
100
+
101
+ <% versions.each do |v| %>
102
+ <% pred = predecessor_of[v] %>
103
+ <% next unless version_changed.call(v, pred) %>
104
+ <dialog id="ck-vdiff-<%= v.id %>" class="ck-modal" onclick="if(event.target===this)this.close()">
105
+ <article class="ck-modal__panel" tabindex="-1" autofocus onclick="event.stopPropagation()">
106
+ <header class="ck-modal__header">
107
+ <div class="ck-modal__heading">
108
+ <h2 class="ck-modal__title">v<%= pred.version_number %> &rarr; v<%= v.version_number %></h2>
109
+ <span class="ck-modal__meta">What changed in <%= v.version_label %><% if v.current? %> (published)<% end %></span>
110
+ </div>
111
+ <button type="button" class="ck-modal__close" aria-label="Close" onclick="this.closest('dialog').close()">&times;</button>
112
+ </header>
113
+ <div class="ck-modal__body">
114
+ <% if pred.llm_model != v.llm_model %>
115
+ <p class="ck-version-change">
116
+ <span class="ck-version-change__label">Model</span>
117
+ <span class="ck-version-change__old"><%= pred.llm_model %></span>
118
+ <span class="ck-version-change__arrow" aria-hidden="true">&rarr;</span>
119
+ <span class="ck-version-change__new"><%= v.llm_model %></span>
120
+ </p>
121
+ <% end %>
122
+ <% if pred.template != v.template %>
123
+ <div class="ck-suggest-diff">
124
+ <div class="ck-suggest-diff__pane">
125
+ <div class="ck-suggest-diff__header"><span class="ck-suggest-diff__label ck-suggest-diff__label--before"><%= pred.version_label %></span></div>
126
+ <pre class="ck-suggest-diff__code"><%= ck_word_diff_old(pred.template, v.template) %></pre>
127
+ </div>
128
+ <div class="ck-suggest-diff__pane">
129
+ <div class="ck-suggest-diff__header"><span class="ck-suggest-diff__label ck-suggest-diff__label--after"><%= v.version_label %></span></div>
130
+ <pre class="ck-suggest-diff__code"><%= ck_word_diff_new(pred.template, v.template) %></pre>
131
+ </div>
132
+ </div>
133
+ <% end %>
134
+ </div>
135
+ </article>
136
+ </dialog>
137
+ <% end %>
81
138
  <% end %>
82
139
 
83
140
  <% if @runs.any? %>
@@ -41,26 +41,31 @@
41
41
  </tr>
42
42
  </thead>
43
43
  <tbody>
44
- <% models.each do |m| %>
45
- <tr>
46
- <td class="ck-model-table__name"><%= m.display_name || m.model_id %></td>
47
- <td class="ck-model-table__cap">
48
- <% if m.supports_generation %>
49
- <span class="ck-model-table__tick" aria-label="Supports generation">✓</span>
50
- <% else %>
51
- <span class="ck-model-table__dash" aria-label="No generation support">—</span>
52
- <% end %>
53
- </td>
54
- <td class="ck-model-table__cap">
55
- <% if m.supports_judging %>
56
- <span class="ck-model-table__tick" aria-label="Supports judging">✓</span>
57
- <% elsif m.supports_judging.nil? %>
58
- <span class="ck-model-table__unknown" aria-label="Untested as judge" title="Untested as a judge — selectable; a successful run confirms it">?</span>
59
- <% else %>
60
- <span class="ck-model-table__dash" aria-label="Not usable as judge">—</span>
61
- <% end %>
62
- </td>
63
- </tr>
44
+ <% ck_model_table_sections(models).each do |section_label, section_models| %>
45
+ <% if section_label %>
46
+ <tr class="ck-model-table__section"><td colspan="3"><%= section_label %></td></tr>
47
+ <% end %>
48
+ <% section_models.each do |m| %>
49
+ <tr>
50
+ <td class="ck-model-table__name"><%= m.display_name || m.model_id %></td>
51
+ <td class="ck-model-table__cap">
52
+ <% if m.supports_generation %>
53
+ <span class="ck-model-table__tick" aria-label="Supports generation">✓</span>
54
+ <% else %>
55
+ <span class="ck-model-table__dash" aria-label="No generation support">—</span>
56
+ <% end %>
57
+ </td>
58
+ <td class="ck-model-table__cap">
59
+ <% if m.supports_judging %>
60
+ <span class="ck-model-table__tick" aria-label="Supports judging">✓</span>
61
+ <% elsif m.supports_judging.nil? %>
62
+ <span class="ck-model-table__unknown" aria-label="Untested as judge" title="Untested as a judge — selectable; a successful run confirms it">?</span>
63
+ <% else %>
64
+ <span class="ck-model-table__dash" aria-label="Not usable as judge">—</span>
65
+ <% end %>
66
+ </td>
67
+ </tr>
68
+ <% end %>
64
69
  <% end %>
65
70
  </tbody>
66
71
  </table>
@@ -27,6 +27,7 @@
27
27
 
28
28
  <div class="ck-provider-card__meta">
29
29
  <span><%= provider_credential.api_endpoint.presence || default_endpoints[provider_credential.provider] %></span>
30
+ <span><%= provider_credential.model_count %> models</span>
30
31
  <span><%= provider_credential.prompt_count %> prompts</span>
31
32
  <span><%= provider_credential.judge_count %> judges</span>
32
33
  <span><% if provider_credential.last_used_at %>Used <time data-relative-time datetime="<%= provider_credential.last_used_at.utc.iso8601 %>"><%= time_ago_in_words(provider_credential.last_used_at) %></time> ago<% else %>Never used<% end %></span>
@@ -74,7 +74,8 @@
74
74
  <% if available.any? %>
75
75
  <div class="ck-select-with-action">
76
76
  <%= form.select :judge_model, ck_grouped_models(available, run.judge_model), { include_blank: "None" }, { class: "ck-input", id: "run_judge_model" } %>
77
- <button type="button" class="ck-icon-btn" title="Refresh models" 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>
77
+ <% ck_refreshing = CompletionKit::ProviderCredential.discovery_in_progress? %>
78
+ <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>
78
79
  </div>
79
80
  <p class="ck-field-hint" id="judge-hint"></p>
80
81
  <div hidden data-refresh-progress-carriers>
@@ -80,7 +80,7 @@
80
80
 
81
81
  <% if @run.dataset %>
82
82
  <dialog id="dataset-preview-<%= @run.id %>" class="ck-modal" onclick="if(event.target===this)this.close()">
83
- <article class="ck-modal__panel" onclick="event.stopPropagation()">
83
+ <article class="ck-modal__panel" tabindex="-1" autofocus onclick="event.stopPropagation()">
84
84
  <header class="ck-modal__header">
85
85
  <div class="ck-modal__heading">
86
86
  <h2 class="ck-modal__title"><%= @run.dataset.name %></h2>
@@ -45,8 +45,9 @@
45
45
  <main class="ck-main">
46
46
  <div class="ck-wrap">
47
47
  <% flash.each do |type, message| %>
48
- <div class="ck-flash <%= type.to_s == "notice" ? "ck-flash--notice" : "ck-flash--alert" %>" role="<%= type.to_s == "notice" ? "status" : "alert" %>">
49
- <%= message %>
48
+ <% notice = type.to_s == "notice" %>
49
+ <div class="ck-flash <%= notice ? "ck-flash--notice" : "ck-flash--alert" %>" role="<%= notice ? "status" : "alert" %>">
50
+ <span class="ck-flash__label"><%= notice ? "Notice" : "Alert" %></span><span class="ck-flash__body"><%= message %></span>
50
51
  </div>
51
52
  <% end %>
52
53
 
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.5.5"
2
+ VERSION = "0.5.6"
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.5.5
4
+ version: 0.5.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin