completion-kit 0.4.8 → 0.5.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 (67) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +12 -0
  3. data/app/assets/config/completion_kit_manifest.js +1 -0
  4. data/app/assets/javascripts/completion_kit/application.js +157 -0
  5. data/app/assets/stylesheets/completion_kit/application.css +382 -0
  6. data/app/controllers/completion_kit/api/v1/datasets_controller.rb +2 -2
  7. data/app/controllers/completion_kit/api/v1/metric_groups_controller.rb +2 -2
  8. data/app/controllers/completion_kit/api/v1/metrics_controller.rb +3 -2
  9. data/app/controllers/completion_kit/api/v1/prompts_controller.rb +5 -4
  10. data/app/controllers/completion_kit/api/v1/runs_controller.rb +3 -2
  11. data/app/controllers/completion_kit/api/v1/tags_controller.rb +51 -0
  12. data/app/controllers/completion_kit/datasets_controller.rb +3 -2
  13. data/app/controllers/completion_kit/metric_groups_controller.rb +7 -6
  14. data/app/controllers/completion_kit/metrics_controller.rb +4 -2
  15. data/app/controllers/completion_kit/prompts_controller.rb +7 -4
  16. data/app/controllers/completion_kit/runs_controller.rb +4 -3
  17. data/app/controllers/completion_kit/tags_controller.rb +50 -0
  18. data/app/controllers/concerns/completion_kit/tag_filtering.rb +22 -0
  19. data/app/helpers/completion_kit/application_helper.rb +11 -0
  20. data/app/models/completion_kit/dataset.rb +5 -2
  21. data/app/models/completion_kit/metric.rb +4 -1
  22. data/app/models/completion_kit/metric_group.rb +4 -1
  23. data/app/models/completion_kit/prompt.rb +4 -1
  24. data/app/models/completion_kit/run.rb +3 -1
  25. data/app/models/completion_kit/tag.rb +39 -0
  26. data/app/models/completion_kit/tagging.rb +12 -0
  27. data/app/models/concerns/completion_kit/taggable.rb +24 -0
  28. data/app/services/completion_kit/mcp_dispatcher.rb +3 -1
  29. data/app/services/completion_kit/mcp_tools/datasets.rb +6 -4
  30. data/app/services/completion_kit/mcp_tools/metric_groups.rb +6 -2
  31. data/app/services/completion_kit/mcp_tools/metrics.rb +8 -4
  32. data/app/services/completion_kit/mcp_tools/prompts.rb +10 -5
  33. data/app/services/completion_kit/mcp_tools/runs.rb +7 -3
  34. data/app/services/completion_kit/mcp_tools/tags.rb +74 -0
  35. data/app/views/completion_kit/api_reference/index.html.erb +38 -0
  36. data/app/views/completion_kit/datasets/_form.html.erb +20 -1
  37. data/app/views/completion_kit/datasets/index.html.erb +17 -1
  38. data/app/views/completion_kit/datasets/show.html.erb +6 -0
  39. data/app/views/completion_kit/metric_groups/_form.html.erb +74 -19
  40. data/app/views/completion_kit/metric_groups/index.html.erb +30 -4
  41. data/app/views/completion_kit/metrics/_form.html.erb +19 -1
  42. data/app/views/completion_kit/metrics/index.html.erb +18 -2
  43. data/app/views/completion_kit/metrics/show.html.erb +6 -0
  44. data/app/views/completion_kit/prompts/_form.html.erb +20 -1
  45. data/app/views/completion_kit/prompts/index.html.erb +17 -1
  46. data/app/views/completion_kit/prompts/show.html.erb +6 -0
  47. data/app/views/completion_kit/provider_credentials/_form.html.erb +1 -1
  48. data/app/views/completion_kit/provider_credentials/index.html.erb +3 -1
  49. data/app/views/completion_kit/runs/_form.html.erb +25 -3
  50. data/app/views/completion_kit/runs/_row.html.erb +5 -0
  51. data/app/views/completion_kit/runs/index.html.erb +9 -0
  52. data/app/views/completion_kit/runs/show.html.erb +6 -0
  53. data/app/views/completion_kit/shared/_settings_nav.html.erb +9 -0
  54. data/app/views/completion_kit/tags/_filter_bar.html.erb +15 -0
  55. data/app/views/completion_kit/tags/_form.html.erb +40 -0
  56. data/app/views/completion_kit/tags/_marks.html.erb +3 -0
  57. data/app/views/completion_kit/tags/_picker.html.erb +20 -0
  58. data/app/views/completion_kit/tags/edit.html.erb +20 -0
  59. data/app/views/completion_kit/tags/index.html.erb +45 -0
  60. data/app/views/completion_kit/tags/new.html.erb +20 -0
  61. data/app/views/layouts/completion_kit/application.html.erb +11 -132
  62. data/config/routes.rb +2 -0
  63. data/db/migrate/20260509000001_create_completion_kit_tags.rb +10 -0
  64. data/db/migrate/20260509000002_create_completion_kit_taggings.rb +16 -0
  65. data/lib/completion_kit/engine.rb +5 -1
  66. data/lib/completion_kit/version.rb +1 -1
  67. metadata +19 -1
@@ -27,7 +27,7 @@
27
27
  </div>
28
28
 
29
29
  <div class="ck-actions">
30
- <%= link_to "Cancel", provider_credentials_path, class: ck_button_classes(:light, variant: :outline) %>
30
+ <%= link_to "Cancel", provider_credentials_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
31
31
  <%= form.submit(provider_credential.persisted? ? "Save provider" : "Create provider", class: ck_button_classes(:dark)) %>
32
32
  </div>
33
33
 
@@ -2,9 +2,11 @@
2
2
  <%= turbo_stream_from "completion_kit_provider_#{pc.id}" %>
3
3
  <% end %>
4
4
 
5
+ <%= render "completion_kit/shared/settings_nav", section_label: "Providers" %>
6
+
5
7
  <section class="ck-page-header">
6
8
  <div>
7
- <h1 class="ck-title">Settings</h1>
9
+ <h1 class="ck-title">Providers</h1>
8
10
  <p class="ck-lead">API keys for LLM providers. These are used for both generating prompt completions and running judge evaluations.</p>
9
11
  </div>
10
12
  <div class="ck-actions">
@@ -115,9 +115,24 @@
115
115
  </div>
116
116
  <div class="ck-metric-divider"><span>or pick individually</span></div>
117
117
  <% end %>
118
- <div class="ck-metric-checkboxes">
118
+
119
+ <% available_filter_tags = @all_metrics.flat_map(&:tags).uniq.sort_by(&:name) %>
120
+ <% if available_filter_tags.any? %>
121
+ <div class="ck-metric-tag-filter" data-metric-tag-filter>
122
+ <span class="ck-metric-tag-filter__label">Filter metrics by tag</span>
123
+ <% available_filter_tags.each do |tag| %>
124
+ <button type="button"
125
+ class="tag-mark tag-mark--off ck-metric-tag-filter__chip"
126
+ style="--mark-color: var(--tag-<%= tag.color %>);"
127
+ data-filter-tag="<%= tag.name %>"
128
+ onclick="ckToggleMetricGroupFilter(this)"><%= tag.name %></button>
129
+ <% end %>
130
+ </div>
131
+ <% end %>
132
+
133
+ <div class="ck-metric-checkboxes" data-metric-checkboxes>
119
134
  <% @all_metrics.each do |metric| %>
120
- <label class="ck-checkbox-label">
135
+ <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>">
121
136
  <%= check_box_tag "run[metric_ids][]", metric.id, run.metric_ids.include?(metric.id), class: "ck-checkbox", id: "run_metric_#{metric.id}" %>
122
137
  <span class="ck-checkbox-label__box" aria-hidden="true"></span>
123
138
  <span class="ck-checkbox-label__body">
@@ -125,6 +140,11 @@
125
140
  <% if metric.instruction.present? %>
126
141
  <span class="ck-checkbox-label__hint"><%= truncate(metric.instruction.to_s, length: 90) %></span>
127
142
  <% end %>
143
+ <% if metric.tags.any? %>
144
+ <div class="tag-marks-row">
145
+ <%= render "completion_kit/tags/marks", tags: metric.tags %>
146
+ </div>
147
+ <% end %>
128
148
  </span>
129
149
  </label>
130
150
  <% end %>
@@ -248,8 +268,10 @@ document.querySelectorAll('input[name="run[metric_ids][]"]').forEach(function(cb
248
268
  updateRunForm();
249
269
  </script>
250
270
 
271
+ <%= render "completion_kit/tags/picker", record: run, param_namespace: :run %>
272
+
251
273
  <div class="ck-actions">
252
- <%= link_to "Cancel", run.persisted? ? run_path(run) : runs_path, class: ck_button_classes(:light, variant: :outline) %>
274
+ <%= link_to "Cancel", run.persisted? ? run_path(run) : runs_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
253
275
  <%= form.submit(run.persisted? ? "Save run" : "Create run", class: ck_button_classes(:dark), id: "run-submit") %>
254
276
  </div>
255
277
  </div>
@@ -13,6 +13,11 @@
13
13
  <%= link_to run.dataset.name, dataset_path(run.dataset), class: "ck-runs-table__config-link", onclick: "event.stopPropagation();" %>
14
14
  <% end %>
15
15
  </div>
16
+ <% if run.tags.any? %>
17
+ <div class="tag-marks-row">
18
+ <%= render "completion_kit/tags/marks", tags: run.tags %>
19
+ </div>
20
+ <% end %>
16
21
  </div>
17
22
  </td>
18
23
  <td>
@@ -8,6 +8,11 @@
8
8
  </div>
9
9
  </section>
10
10
 
11
+ <%= render "completion_kit/tags/filter_bar",
12
+ available: @available_tags,
13
+ selected: @selected_tags,
14
+ base_path: runs_path %>
15
+
11
16
  <% if @runs.any? %>
12
17
  <table class="ck-results-table ck-runs-table">
13
18
  <thead>
@@ -26,6 +31,10 @@
26
31
  <% end %>
27
32
  </tbody>
28
33
  </table>
34
+ <% elsif @selected_tags.any? %>
35
+ <div class="ck-empty">
36
+ <p>No runs match these tags. <%= link_to "Clear filters", runs_path, class: "ck-link" %>.</p>
37
+ </div>
29
38
  <% else %>
30
39
  <div class="ck-empty">No runs yet.&ensp;<%= link_to "Create your first run →", new_run_path, class: "ck-link" %></div>
31
40
  <% end %>
@@ -7,6 +7,12 @@
7
7
 
8
8
  <%= render "status_header", run: @run %>
9
9
 
10
+ <% if @run.tags.any? %>
11
+ <div class="tag-marks-row tag-marks-row--header">
12
+ <%= render "completion_kit/tags/marks", tags: @run.tags %>
13
+ </div>
14
+ <% end %>
15
+
10
16
  <% if @run.dataset %>
11
17
  <% dataset_lines = @run.dataset.csv_data.to_s.lines %>
12
18
  <% dataset_preview_lines = dataset_lines.first(50) %>
@@ -0,0 +1,9 @@
1
+ <p class="ck-settings-kicker">
2
+ Settings <span class="ck-settings-kicker__sep">/</span>
3
+ <% link = local_assigns[:section_link] %>
4
+ <%= link ? link_to(section_label, link, class: "ck-settings-kicker__link") : section_label %>
5
+ <% if local_assigns[:trail].present? %>
6
+ <span class="ck-settings-kicker__sep">/</span>
7
+ <%= trail.html_safe %>
8
+ <% end %>
9
+ </p>
@@ -0,0 +1,15 @@
1
+ <% if available.any? %>
2
+ <div class="ck-tag-filter">
3
+ <span class="ck-tag-filter__label">Filter by tag</span>
4
+ <% available.each do |tag| %>
5
+ <% on = selected.any? { |s| s.id == tag.id } %>
6
+ <%= link_to tag.name,
7
+ tag_filter_url(base_path, selected, tag),
8
+ class: ["tag-mark", ("tag-mark--off" unless on)].compact.join(" "),
9
+ style: "--mark-color: var(--tag-#{tag.color});" %>
10
+ <% end %>
11
+ <% if selected.any? %>
12
+ <%= link_to "× clear", base_path, class: "ck-tag-filter__clear" %>
13
+ <% end %>
14
+ </div>
15
+ <% end %>
@@ -0,0 +1,40 @@
1
+ <%= form_with(model: tag, local: true) do |form| %>
2
+ <div class="ck-card ck-form-card">
3
+ <% name_error = tag.errors[:name].first %>
4
+ <div class="ck-field">
5
+ <%= form.label :name, "Tag name", class: "ck-label" %>
6
+ <%= form.text_field :name,
7
+ id: "tag_name",
8
+ class: ["ck-input", ("ck-input--error" if name_error)].compact.join(" "),
9
+ placeholder: "e.g. production",
10
+ autofocus: true,
11
+ "aria-invalid": name_error ? "true" : nil %>
12
+ <% if name_error %>
13
+ <p class="ck-field-error" role="alert"><%= name_error %></p>
14
+ <% elsif !tag.persisted? %>
15
+ <p class="ck-hint">Color is auto-assigned from a 10-color palette.</p>
16
+ <% end %>
17
+ </div>
18
+
19
+ <div class="ck-actions">
20
+ <% if tag.persisted? %>
21
+ <% applied_n = tag.taggings.count %>
22
+ <% confirm = if applied_n.zero?
23
+ "Delete \"#{tag.name}\"? It's not currently applied to anything."
24
+ else
25
+ "Delete \"#{tag.name}\"? It's currently applied to #{pluralize(applied_n, 'item')} — they'll lose this tag."
26
+ end %>
27
+ <%= button_to tag_path(tag), method: :delete,
28
+ form_class: "inline-block",
29
+ class: "ck-icon-btn",
30
+ title: "Delete tag",
31
+ "aria-label": "Delete tag",
32
+ data: { turbo_confirm: confirm } do %>
33
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
34
+ <% end %>
35
+ <% end %>
36
+ <%= link_to "Cancel", tags_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
37
+ <%= form.submit(tag.persisted? ? "Save tag" : "Create tag", class: ck_button_classes(:dark)) %>
38
+ </div>
39
+ </div>
40
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <% Array(tags).each do |tag| %>
2
+ <span class="tag-mark" style="--mark-color: var(--tag-<%= tag.color %>);"><%= tag.name %></span>
3
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <div class="ck-field">
2
+ <% label_text = local_assigns[:label].presence || "#{param_namespace.to_s.titleize} tags" %>
3
+ <%= label_tag "#{param_namespace}_tags", label_text, class: "ck-label" %>
4
+ <% all_tags = CompletionKit::Tag.order(:name) %>
5
+ <% selected_ids = record.persisted? ? record.tags.pluck(:id) : [] %>
6
+ <div class="ck-tag-picker">
7
+ <% all_tags.each do |tag| %>
8
+ <% checked = selected_ids.include?(tag.id) %>
9
+ <label class="tag-mark" style="--mark-color: var(--tag-<%= tag.color %>);">
10
+ <%= check_box_tag "#{param_namespace}[tag_names][]", tag.name, checked, hidden: true %>
11
+ <%= tag.name %>
12
+ </label>
13
+ <% end %>
14
+ <%= text_field_tag "#{param_namespace}[tag_names][]", "",
15
+ id: "#{param_namespace}_tags",
16
+ class: "ck-tag-picker__input",
17
+ placeholder: all_tags.any? ? "+ create tag" : "+ create first tag",
18
+ autocomplete: "off" %>
19
+ </div>
20
+ </div>
@@ -0,0 +1,20 @@
1
+ <% original = @tag.name_was.presence || "tag" %>
2
+ <% display_name = @tag.name.presence || original %>
3
+ <% pill = content_tag(:span,
4
+ content_tag(:span, display_name, id: "tag-pill-text", data: { placeholder: original }),
5
+ id: "tag-breadcrumb-pill",
6
+ class: "tag-mark",
7
+ style: "--mark-color: var(--tag-#{@tag.color});") %>
8
+
9
+ <%= render "completion_kit/shared/settings_nav",
10
+ section_label: "Tags",
11
+ section_link: tags_path,
12
+ trail: pill %>
13
+
14
+ <section class="ck-page-header">
15
+ <div>
16
+ <h1 class="ck-title">Edit tag</h1>
17
+ </div>
18
+ </section>
19
+
20
+ <%= render "form", tag: @tag %>
@@ -0,0 +1,45 @@
1
+ <%= render "completion_kit/shared/settings_nav", section_label: "Tags" %>
2
+
3
+ <section class="ck-page-header">
4
+ <div>
5
+ <h1 class="ck-title">Tags</h1>
6
+ <p class="ck-lead">Domain labels for organizing metrics, prompts, runs, and datasets.</p>
7
+ </div>
8
+ <div class="ck-actions">
9
+ <%= link_to "New tag", new_tag_path, class: ck_button_classes(:dark) %>
10
+ </div>
11
+ </section>
12
+
13
+ <% if @tags.any? %>
14
+ <table class="ck-results-table ck-tags-table">
15
+ <thead>
16
+ <tr>
17
+ <th>Tag</th>
18
+ <th>Applied to</th>
19
+ <th></th>
20
+ </tr>
21
+ </thead>
22
+ <tbody>
23
+ <% @tags.each do |tag| %>
24
+ <% count = @tagging_counts.fetch(tag.id, 0) %>
25
+ <% by_type = @tagging_by_type.select { |(tid, _), _| tid == tag.id } %>
26
+ <% breakdown = by_type.map { |(_, type), n| pluralize(n, type.demodulize.titleize.downcase) }.join(" · ") %>
27
+ <tr onclick="window.location='<%= edit_tag_path(tag) %>'" style="cursor: pointer;">
28
+ <td><span class="tag-mark tag-mark--lg" style="--mark-color: var(--tag-<%= tag.color %>);"><%= tag.name %></span></td>
29
+ <td class="ck-meta-copy">
30
+ <% if count.zero? %>
31
+ <span class="ck-tags-table__unused">Not used yet</span>
32
+ <% else %>
33
+ <%= breakdown %>
34
+ <% end %>
35
+ </td>
36
+ <td class="ck-results-table__arrow">&rarr;</td>
37
+ </tr>
38
+ <% end %>
39
+ </tbody>
40
+ </table>
41
+ <% else %>
42
+ <div class="ck-empty">
43
+ <p>No tags yet. <%= link_to "Create your first tag", new_tag_path, class: "ck-link" %> to start organizing.</p>
44
+ </div>
45
+ <% end %>
@@ -0,0 +1,20 @@
1
+ <% display_name = @tag.name.presence.to_s %>
2
+ <% pill = content_tag(:span,
3
+ content_tag(:span, display_name, id: "tag-pill-text", data: { placeholder: "" }),
4
+ id: "tag-breadcrumb-pill",
5
+ class: "tag-mark",
6
+ style: "--mark-color: var(--tag-#{@tag.color});") %>
7
+
8
+ <%= render "completion_kit/shared/settings_nav",
9
+ section_label: "Tags",
10
+ section_link: tags_path,
11
+ trail: pill %>
12
+
13
+ <section class="ck-page-header">
14
+ <div>
15
+ <h1 class="ck-title">New tag</h1>
16
+ <p class="ck-lead">Tags help you filter metrics, prompts, runs, and datasets by domain.</p>
17
+ </div>
18
+ </section>
19
+
20
+ <%= render "form", tag: @tag %>
@@ -9,6 +9,7 @@
9
9
  <%= favicon_link_tag "completion_kit/logo.svg", type: "image/svg+xml" %>
10
10
  <%= stylesheet_link_tag "completion_kit/application", media: "all" %>
11
11
  <%= javascript_include_tag "turbo", type: "module" %>
12
+ <%= javascript_include_tag "completion_kit/application", defer: true %>
12
13
  <%= action_cable_meta_tag %>
13
14
  </head>
14
15
  <body class="ck-app">
@@ -22,7 +23,16 @@
22
23
  <%= link_to "Metrics", metrics_path, class: request.path.start_with?(metrics_path) || request.path.start_with?(metric_groups_path) ? ck_button_classes(:dark) : ck_button_classes(:light, variant: :outline) %>
23
24
  <%= link_to "Datasets", datasets_path, class: active.(datasets_path) %>
24
25
  <%= link_to "Runs", runs_path, class: active.(runs_path) %>
25
- <%= link_to "Settings", provider_credentials_path, class: active.(provider_credentials_path) %>
26
+ <% settings_active = request.path.start_with?(provider_credentials_path) || request.path.start_with?(tags_path) %>
27
+ <details class="ck-settings-menu">
28
+ <summary class="<%= settings_active ? ck_button_classes(:dark) : ck_button_classes(:light, variant: :outline) %> ck-settings-menu__trigger" aria-label="Settings">
29
+ Settings
30
+ </summary>
31
+ <div class="ck-settings-menu__panel" role="menu">
32
+ <%= link_to "Providers", provider_credentials_path, class: "ck-settings-menu__item" %>
33
+ <%= link_to "Tags", tags_path, class: "ck-settings-menu__item" %>
34
+ </div>
35
+ </details>
26
36
  <%= link_to "API", api_reference_path, class: active.(api_reference_path) %>
27
37
  <% if main_app.respond_to?(:logout_path) %>
28
38
  <%= button_to "Log out", main_app.logout_path, method: :delete, class: ck_button_classes(:light, variant: :outline) %>
@@ -42,136 +52,5 @@
42
52
  <%= yield %>
43
53
  </div>
44
54
  </main>
45
- <script>
46
- document.addEventListener("turbo:load", function() {
47
- document.querySelectorAll("[data-local-time]").forEach(function(el) {
48
- var d = new Date(el.getAttribute("datetime"));
49
- el.textContent = d.toLocaleString(undefined, {year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});
50
- });
51
- ckTickRelativeTimes();
52
- });
53
-
54
- function ckRelativeTime(then) {
55
- var seconds = Math.round((Date.now() - then.getTime()) / 1000);
56
- if (seconds < 5) return "just now";
57
- if (seconds < 60) return "less than a minute";
58
- var minutes = Math.round(seconds / 60);
59
- if (minutes < 60) return minutes === 1 ? "1 minute" : minutes + " minutes";
60
- var hours = Math.round(minutes / 60);
61
- if (hours < 24) return hours === 1 ? "about 1 hour" : "about " + hours + " hours";
62
- var days = Math.round(hours / 24);
63
- if (days < 30) return days === 1 ? "1 day" : days + " days";
64
- var months = Math.round(days / 30);
65
- if (months < 12) return months === 1 ? "about 1 month" : "about " + months + " months";
66
- var years = Math.round(days / 365);
67
- return years === 1 ? "about 1 year" : "about " + years + " years";
68
- }
69
-
70
- function ckRelativeTimeCompact(then) {
71
- var seconds = Math.round((Date.now() - then.getTime()) / 1000);
72
- if (seconds < 60) return "now";
73
- var minutes = Math.round(seconds / 60);
74
- if (minutes < 60) return minutes + "m";
75
- var hours = Math.round(minutes / 60);
76
- if (hours < 24) return hours + "h";
77
- var days = Math.round(hours / 24);
78
- if (days < 30) return days + "d";
79
- var months = Math.round(days / 30);
80
- if (months < 12) return months + "mo";
81
- var years = Math.round(days / 365);
82
- return years + "y";
83
- }
84
-
85
- function ckTickRelativeTimes() {
86
- document.querySelectorAll("[data-relative-time]").forEach(function(el) {
87
- var then = new Date(el.getAttribute("datetime"));
88
- if (isNaN(then.getTime())) return;
89
- var verbose = el.getAttribute("data-relative-time") === "verbose";
90
- el.textContent = verbose ? ckRelativeTime(then) : ckRelativeTimeCompact(then);
91
- el.setAttribute("title", then.toLocaleString());
92
- });
93
- }
94
-
95
- if (!window.ckRelativeTimeInterval) {
96
- window.ckRelativeTimeInterval = setInterval(ckTickRelativeTimes, 30000);
97
- }
98
- document.addEventListener("turbo:before-stream-render", function() {
99
- requestAnimationFrame(ckTickRelativeTimes);
100
- });
101
-
102
- var ckCsvHoverTimer = null;
103
- var ckCsvHoverRow = null;
104
- document.addEventListener("mouseover", function(e) {
105
- var row = e.target.closest && e.target.closest(".ck-csv-table tbody tr");
106
- if (!row || row === ckCsvHoverRow) return;
107
- if (ckCsvHoverRow) ckCsvHoverRow.classList.remove("ck-csv-row--expanded");
108
- ckCsvHoverRow = row;
109
- clearTimeout(ckCsvHoverTimer);
110
- ckCsvHoverTimer = setTimeout(function() {
111
- if (ckCsvHoverRow === row) row.classList.add("ck-csv-row--expanded");
112
- }, 350);
113
- });
114
- document.addEventListener("mouseout", function(e) {
115
- var row = e.target.closest && e.target.closest(".ck-csv-table tbody tr");
116
- if (!row) return;
117
- var related = e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".ck-csv-table tbody tr");
118
- if (related === row) return;
119
- clearTimeout(ckCsvHoverTimer);
120
- row.classList.remove("ck-csv-row--expanded");
121
- if (ckCsvHoverRow === row) ckCsvHoverRow = null;
122
- });
123
-
124
- var ckRefreshing = false;
125
- function ckRefreshModels() {
126
- if (ckRefreshing) return;
127
- ckRefreshing = true;
128
- var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
129
- if (btn) btn.classList.add('ck-icon-btn--spinning');
130
- ckUpdateRefreshProgress();
131
- var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
132
- fetch("/completion_kit/refresh_models", {
133
- method: "POST",
134
- headers: { "X-CSRF-Token": csrfToken }
135
- });
136
- }
137
-
138
- function ckUpdateRefreshProgress() {
139
- var status = document.getElementById('refresh-status');
140
- if (!status) return;
141
- var carriers = document.querySelectorAll('[data-refresh-progress-carriers] [id^="discovery_status_"]');
142
- var totalCurrent = 0, totalTotal = 0, anyDiscovering = false;
143
- carriers.forEach(function(node) {
144
- if (!node.querySelector('.ck-discovery-bar')) return;
145
- if (node.querySelector('.ck-discovery-bar--failed') || node.querySelector('.ck-discovery-bar--completed')) return;
146
- anyDiscovering = true;
147
- var match = node.textContent.match(/(\d+)\s*\/\s*(\d+)/);
148
- if (match) {
149
- totalCurrent += parseInt(match[1], 10);
150
- totalTotal += parseInt(match[2], 10);
151
- }
152
- });
153
- if (anyDiscovering || ckRefreshing) {
154
- if (totalTotal > 0) {
155
- status.textContent = 'Refreshing models\u2026 ' + totalCurrent + '/' + totalTotal;
156
- } else {
157
- status.textContent = 'Refreshing models\u2026';
158
- }
159
- }
160
- }
161
-
162
- document.addEventListener("turbo:before-stream-render", function(event) {
163
- var target = event.target.getAttribute("target");
164
- if (target && target.indexOf("discovery_status_") === 0) {
165
- requestAnimationFrame(ckUpdateRefreshProgress);
166
- }
167
- if (target === "prompt_llm_model" || target === "run_judge_model") {
168
- ckRefreshing = false;
169
- var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
170
- if (btn) btn.classList.remove('ck-icon-btn--spinning');
171
- var status = document.getElementById('refresh-status');
172
- if (status) { status.textContent = 'Models updated.'; setTimeout(function() { status.textContent = '\u00a0'; }, 3000); }
173
- }
174
- });
175
- </script>
176
55
  </body>
177
56
  </html>
data/config/routes.rb CHANGED
@@ -10,6 +10,7 @@ CompletionKit::Engine.routes.draw do
10
10
  resources :datasets
11
11
  resources :metrics
12
12
  resources :metric_groups
13
+ resources :tags
13
14
 
14
15
  resources :runs do
15
16
  member do
@@ -52,6 +53,7 @@ CompletionKit::Engine.routes.draw do
52
53
  resources :datasets
53
54
  resources :metrics
54
55
  resources :metric_groups
56
+ resources :tags
55
57
  resources :provider_credentials
56
58
  end
57
59
  end
@@ -0,0 +1,10 @@
1
+ class CreateCompletionKitTags < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :completion_kit_tags do |t|
4
+ t.string :name, null: false
5
+ t.string :color, null: false
6
+ t.timestamps
7
+ end
8
+ add_index :completion_kit_tags, :name, unique: true
9
+ end
10
+ end
@@ -0,0 +1,16 @@
1
+ class CreateCompletionKitTaggings < ActiveRecord::Migration[8.1]
2
+ def change
3
+ create_table :completion_kit_taggings do |t|
4
+ t.references :tag, null: false,
5
+ foreign_key: { to_table: :completion_kit_tags }
6
+ t.string :taggable_type, null: false
7
+ t.bigint :taggable_id, null: false
8
+ t.timestamps
9
+ end
10
+ add_index :completion_kit_taggings, [:taggable_type, :taggable_id]
11
+ add_index :completion_kit_taggings,
12
+ [:tag_id, :taggable_type, :taggable_id],
13
+ unique: true,
14
+ name: "idx_taggings_unique"
15
+ end
16
+ end
@@ -8,7 +8,11 @@ module CompletionKit
8
8
  paths.add "app/services", eager_load: true
9
9
 
10
10
  def self.register_assets(app)
11
- app.config.assets.precompile += %w( completion_kit/application.css completion_kit/logo.svg )
11
+ app.config.assets.precompile += %w(
12
+ completion_kit/application.css
13
+ completion_kit/application.js
14
+ completion_kit/logo.svg
15
+ )
12
16
  end
13
17
 
14
18
  initializer("completion_kit.assets") { |app| Engine.register_assets(app) }
@@ -1,3 +1,3 @@
1
1
  module CompletionKit
2
- VERSION = "0.4.8"
2
+ VERSION = "0.5.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.4.8
4
+ version: 0.5.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Damien Bastin
@@ -228,6 +228,7 @@ files:
228
228
  - app/assets/config/completion_kit_manifest.js
229
229
  - app/assets/config/manifest.js
230
230
  - app/assets/images/completion_kit/logo.svg
231
+ - app/assets/javascripts/completion_kit/application.js
231
232
  - app/assets/stylesheets/completion_kit/application.css
232
233
  - app/controllers/completion_kit/api/v1/base_controller.rb
233
234
  - app/controllers/completion_kit/api/v1/datasets_controller.rb
@@ -237,6 +238,7 @@ files:
237
238
  - app/controllers/completion_kit/api/v1/provider_credentials_controller.rb
238
239
  - app/controllers/completion_kit/api/v1/responses_controller.rb
239
240
  - app/controllers/completion_kit/api/v1/runs_controller.rb
241
+ - app/controllers/completion_kit/api/v1/tags_controller.rb
240
242
  - app/controllers/completion_kit/api_reference_controller.rb
241
243
  - app/controllers/completion_kit/application_controller.rb
242
244
  - app/controllers/completion_kit/datasets_controller.rb
@@ -248,6 +250,8 @@ files:
248
250
  - app/controllers/completion_kit/responses_controller.rb
249
251
  - app/controllers/completion_kit/runs_controller.rb
250
252
  - app/controllers/completion_kit/suggestions_controller.rb
253
+ - app/controllers/completion_kit/tags_controller.rb
254
+ - app/controllers/concerns/completion_kit/tag_filtering.rb
251
255
  - app/helpers/completion_kit/application_helper.rb
252
256
  - app/jobs/completion_kit/application_job.rb
253
257
  - app/jobs/completion_kit/generate_row_job.rb
@@ -268,6 +272,9 @@ files:
268
272
  - app/models/completion_kit/run.rb
269
273
  - app/models/completion_kit/run_metric.rb
270
274
  - app/models/completion_kit/suggestion.rb
275
+ - app/models/completion_kit/tag.rb
276
+ - app/models/completion_kit/tagging.rb
277
+ - app/models/concerns/completion_kit/taggable.rb
271
278
  - app/services/completion_kit/anthropic_client.rb
272
279
  - app/services/completion_kit/api_config.rb
273
280
  - app/services/completion_kit/csv_processor.rb
@@ -282,6 +289,7 @@ files:
282
289
  - app/services/completion_kit/mcp_tools/provider_credentials.rb
283
290
  - app/services/completion_kit/mcp_tools/responses.rb
284
291
  - app/services/completion_kit/mcp_tools/runs.rb
292
+ - app/services/completion_kit/mcp_tools/tags.rb
285
293
  - app/services/completion_kit/model_discovery_service.rb
286
294
  - app/services/completion_kit/ollama_client.rb
287
295
  - app/services/completion_kit/open_ai_client.rb
@@ -329,7 +337,15 @@ files:
329
337
  - app/views/completion_kit/runs/index.html.erb
330
338
  - app/views/completion_kit/runs/new.html.erb
331
339
  - app/views/completion_kit/runs/show.html.erb
340
+ - app/views/completion_kit/shared/_settings_nav.html.erb
332
341
  - app/views/completion_kit/suggestions/show.html.erb
342
+ - app/views/completion_kit/tags/_filter_bar.html.erb
343
+ - app/views/completion_kit/tags/_form.html.erb
344
+ - app/views/completion_kit/tags/_marks.html.erb
345
+ - app/views/completion_kit/tags/_picker.html.erb
346
+ - app/views/completion_kit/tags/edit.html.erb
347
+ - app/views/completion_kit/tags/index.html.erb
348
+ - app/views/completion_kit/tags/new.html.erb
333
349
  - app/views/layouts/completion_kit/application.html.erb
334
350
  - config/routes.rb
335
351
  - db/migrate/20260311000001_create_completion_kit_tables.rb
@@ -351,6 +367,8 @@ files:
351
367
  - db/migrate/20260501000005_collapse_run_status_and_add_failure_summary.rb
352
368
  - db/migrate/20260507000001_add_discovery_error_to_provider_credentials.rb
353
369
  - db/migrate/20260507150000_add_temperature_ignored_to_runs.rb
370
+ - db/migrate/20260509000001_create_completion_kit_tags.rb
371
+ - db/migrate/20260509000002_create_completion_kit_taggings.rb
354
372
  - lib/completion-kit.rb
355
373
  - lib/completion_kit.rb
356
374
  - lib/completion_kit/concurrency_check.rb