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
@@ -20,7 +20,8 @@ module CompletionKit
20
20
  type: "object",
21
21
  properties: {
22
22
  name: {type: "string"}, description: {type: "string"},
23
- template: {type: "string"}, llm_model: {type: "string"}
23
+ template: {type: "string"}, llm_model: {type: "string"},
24
+ tag_names: {type: "array", items: {type: "string"}}
24
25
  },
25
26
  required: ["name", "template", "llm_model"]
26
27
  },
@@ -32,7 +33,8 @@ module CompletionKit
32
33
  type: "object",
33
34
  properties: {
34
35
  id: {type: "integer"}, name: {type: "string"}, description: {type: "string"},
35
- template: {type: "string"}, llm_model: {type: "string"}
36
+ template: {type: "string"}, llm_model: {type: "string"},
37
+ tag_names: {type: "array", items: {type: "string"}}
36
38
  },
37
39
  required: ["id"]
38
40
  },
@@ -60,8 +62,9 @@ module CompletionKit
60
62
 
61
63
  def self.create(args)
62
64
  prompt = Prompt.new(args.slice("name", "description", "template", "llm_model"))
65
+ prompt.tag_names = args["tag_names"] if args.key?("tag_names")
63
66
  if prompt.save
64
- text_result(prompt.as_json)
67
+ text_result(prompt.reload.as_json)
65
68
  else
66
69
  error_result(prompt.errors.full_messages.join(", "))
67
70
  end
@@ -73,9 +76,11 @@ module CompletionKit
73
76
  if prompt.runs.exists?
74
77
  new_prompt = prompt.clone_as_new_version(attrs)
75
78
  new_prompt.publish!
76
- text_result(new_prompt.as_json)
79
+ new_prompt.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
80
+ text_result(new_prompt.reload.as_json)
77
81
  elsif prompt.update(attrs)
78
- text_result(prompt.as_json)
82
+ prompt.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
83
+ text_result(prompt.reload.as_json)
79
84
  else
80
85
  error_result(prompt.errors.full_messages.join(", "))
81
86
  end
@@ -21,7 +21,8 @@ module CompletionKit
21
21
  properties: {
22
22
  name: {type: "string"}, prompt_id: {type: "integer"},
23
23
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
24
- metric_ids: {type: "array", items: {type: "integer"}}
24
+ metric_ids: {type: "array", items: {type: "integer"}},
25
+ tag_names: {type: "array", items: {type: "string"}}
25
26
  },
26
27
  required: ["name", "prompt_id"]
27
28
  },
@@ -34,7 +35,8 @@ module CompletionKit
34
35
  properties: {
35
36
  id: {type: "integer"}, name: {type: "string"},
36
37
  dataset_id: {type: "integer"}, judge_model: {type: "string"},
37
- metric_ids: {type: "array", items: {type: "integer"}}
38
+ metric_ids: {type: "array", items: {type: "integer"}},
39
+ tag_names: {type: "array", items: {type: "string"}}
38
40
  },
39
41
  required: ["id"]
40
42
  },
@@ -64,6 +66,7 @@ module CompletionKit
64
66
  run = Run.new(args.slice("name", "prompt_id", "dataset_id", "judge_model"))
65
67
  if run.save
66
68
  run.replace_metrics!(args["metric_ids"])
69
+ run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
67
70
  text_result(run.reload.as_json)
68
71
  else
69
72
  error_result(run.errors.full_messages.join(", "))
@@ -72,8 +75,9 @@ module CompletionKit
72
75
 
73
76
  def self.update(args)
74
77
  run = Run.find(args["id"])
75
- if run.update(args.except("id", "metric_ids").slice("name", "dataset_id", "judge_model"))
78
+ if run.update(args.except("id", "metric_ids", "tag_names").slice("name", "dataset_id", "judge_model"))
76
79
  run.replace_metrics!(args["metric_ids"]) if args.key?("metric_ids")
80
+ run.update!(tag_names: args["tag_names"]) if args.key?("tag_names")
77
81
  text_result(run.reload.as_json)
78
82
  else
79
83
  error_result(run.errors.full_messages.join(", "))
@@ -0,0 +1,74 @@
1
+ module CompletionKit
2
+ module McpTools
3
+ module Tags
4
+ extend Base
5
+
6
+ TOOLS = {
7
+ "tags_list" => {
8
+ description: "List all tags",
9
+ inputSchema: {type: "object", properties: {}, required: []},
10
+ handler: :list
11
+ },
12
+ "tags_get" => {
13
+ description: "Get a tag by ID",
14
+ inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
15
+ handler: :get
16
+ },
17
+ "tags_create" => {
18
+ description: "Create a tag. Color is auto-assigned.",
19
+ inputSchema: {
20
+ type: "object",
21
+ properties: {name: {type: "string"}},
22
+ required: ["name"]
23
+ },
24
+ handler: :create
25
+ },
26
+ "tags_update" => {
27
+ description: "Rename a tag.",
28
+ inputSchema: {
29
+ type: "object",
30
+ properties: {id: {type: "integer"}, name: {type: "string"}},
31
+ required: ["id"]
32
+ },
33
+ handler: :update
34
+ },
35
+ "tags_delete" => {
36
+ description: "Delete a tag. Removes the tag from every linked metric, prompt, run, and dataset.",
37
+ inputSchema: {type: "object", properties: {id: {type: "integer"}}, required: ["id"]},
38
+ handler: :delete
39
+ }
40
+ }.freeze
41
+
42
+ def self.list(_args)
43
+ text_result(CompletionKit::Tag.order(:name).map(&:as_json))
44
+ end
45
+
46
+ def self.get(args)
47
+ text_result(CompletionKit::Tag.find(args["id"]).as_json)
48
+ end
49
+
50
+ def self.create(args)
51
+ tag = CompletionKit::Tag.new(name: args["name"])
52
+ if tag.save
53
+ text_result(tag.as_json)
54
+ else
55
+ error_result(tag.errors.full_messages.join(", "))
56
+ end
57
+ end
58
+
59
+ def self.update(args)
60
+ tag = CompletionKit::Tag.find(args["id"])
61
+ if tag.update(name: args["name"])
62
+ text_result(tag.as_json)
63
+ else
64
+ error_result(tag.errors.full_messages.join(", "))
65
+ end
66
+ end
67
+
68
+ def self.delete(args)
69
+ CompletionKit::Tag.find(args["id"]).destroy!
70
+ text_result("Tag #{args["id"]} deleted")
71
+ end
72
+ end
73
+ end
74
+ end
@@ -64,6 +64,7 @@ end %>
64
64
  <input type="radio" name="ck-api-tab" id="ck-tab-datasets" class="ck-api-tabs__radio">
65
65
  <input type="radio" name="ck-api-tab" id="ck-tab-metrics" class="ck-api-tabs__radio">
66
66
  <input type="radio" name="ck-api-tab" id="ck-tab-metric-groups" class="ck-api-tabs__radio">
67
+ <input type="radio" name="ck-api-tab" id="ck-tab-tags" class="ck-api-tabs__radio">
67
68
  <input type="radio" name="ck-api-tab" id="ck-tab-providers" class="ck-api-tabs__radio">
68
69
 
69
70
  <nav class="ck-api-tabs__nav">
@@ -74,6 +75,7 @@ end %>
74
75
  <label for="ck-tab-datasets" class="ck-api-tabs__label">Datasets <span class="ck-api-tabs__count">5</span></label>
75
76
  <label for="ck-tab-metrics" class="ck-api-tabs__label">Metrics <span class="ck-api-tabs__count">5</span></label>
76
77
  <label for="ck-tab-metric-groups" class="ck-api-tabs__label">Metric Groups <span class="ck-api-tabs__count">5</span></label>
78
+ <label for="ck-tab-tags" class="ck-api-tabs__label">Tags <span class="ck-api-tabs__count">5</span></label>
77
79
  <label for="ck-tab-providers" class="ck-api-tabs__label">Providers <span class="ck-api-tabs__count">5</span></label>
78
80
  </nav>
79
81
 
@@ -254,6 +256,42 @@ end %>
254
256
  </div>
255
257
  </div>
256
258
 
259
+ <div class="ck-api-tabs__panel">
260
+ <h2 class="ck-section-title">Tags</h2>
261
+ <p class="ck-copy">Domain labels you can attach to metrics, prompts, runs, and datasets. Tags are auto-assigned a color from a 10-color palette. Each index page can be filtered by one or more tags using <code>?tag[]=name</code> query params (OR semantics).</p>
262
+ <div class="ck-api-endpoint">
263
+ <p class="ck-api-method"><span class="ck-chip ck-chip--soft">GET</span> /api/v1/tags</p>
264
+ <p class="ck-meta-copy">List all tags with name and color.</p>
265
+ <%= render "example", base_url: @base_url, token: token_display, real_token: @token, cmd: "curl #{@base_url}/api/v1/tags \\\n -H \"Authorization: Bearer #{token_display}\"" %>
266
+ </div>
267
+ <div class="ck-api-endpoint">
268
+ <p class="ck-api-method"><span class="ck-chip ck-chip--soft">POST</span> /api/v1/tags</p>
269
+ <p class="ck-meta-copy">Create a tag.</p>
270
+ <p class="ck-api-params"><strong>Required:</strong>&ensp;<code>name</code></p>
271
+ <%= render "example", base_url: @base_url, token: token_display, real_token: @token, cmd: "curl -X POST #{@base_url}/api/v1/tags \\\n -H \"Authorization: Bearer #{token_display}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"real estate\"}'" %>
272
+ </div>
273
+ <div class="ck-api-endpoint">
274
+ <p class="ck-api-method"><span class="ck-chip ck-chip--soft">GET</span>&ensp;<span class="ck-chip ck-chip--soft">PATCH</span>&ensp;<span class="ck-chip" style="color: var(--ck-danger);">DELETE</span> /api/v1/tags/:id</p>
275
+ <p class="ck-meta-copy">Get, update, or delete a tag. PATCH accepts <code>name</code>. DELETE returns 204 No Content and removes all taggings for this tag.</p>
276
+ </div>
277
+ <div class="ck-api-endpoint" style="padding-top: 1rem;">
278
+ <p class="ck-kicker" style="margin-bottom: 0.5rem;">Tagging resources</p>
279
+ <p class="ck-meta-copy">Metrics, prompts, runs, and datasets accept a <code>tag_names</code> array on their create and update endpoints. Passing a name that does not yet exist silently creates the tag. On PATCH, the list replaces all existing tags for that record (omit the field to leave tags unchanged).</p>
280
+ <%= render "example", base_url: @base_url, token: token_display, real_token: @token, cmd: "curl -X POST #{@base_url}/api/v1/metrics \\\n -H \"Authorization: Bearer #{token_display}\" \\\n -H \"Content-Type: application/json\" \\\n -d '{\"name\": \"Accuracy\", \"tag_names\": [\"real estate\"]}'" %>
281
+ </div>
282
+ <div class="ck-api-endpoint" style="padding-top: 1rem;">
283
+ <p class="ck-kicker" style="margin-bottom: 0.5rem;">MCP tools</p>
284
+ <div class="ck-mcp-tools">
285
+ <div class="ck-mcp-tool"><code class="ck-mcp-tool__name">tags_list</code><span class="ck-mcp-tool__desc">List all tags</span></div>
286
+ <div class="ck-mcp-tool"><code class="ck-mcp-tool__name">tags_get</code><span class="ck-mcp-tool__desc">Get a tag by ID</span></div>
287
+ <div class="ck-mcp-tool"><code class="ck-mcp-tool__name">tags_create</code><span class="ck-mcp-tool__desc">Create a tag (name required)</span></div>
288
+ <div class="ck-mcp-tool"><code class="ck-mcp-tool__name">tags_update</code><span class="ck-mcp-tool__desc">Update a tag's name</span></div>
289
+ <div class="ck-mcp-tool"><code class="ck-mcp-tool__name">tags_delete</code><span class="ck-mcp-tool__desc">Delete a tag and remove all its taggings</span></div>
290
+ </div>
291
+ <p class="ck-meta-copy" style="margin-top: 0.75rem;">The existing <code>metrics_create</code>, <code>metrics_update</code>, <code>prompts_create</code>, <code>prompts_update</code>, <code>runs_create</code>, <code>runs_update</code>, <code>datasets_create</code>, and <code>datasets_update</code> tools all accept a <code>tag_names</code> parameter with the same auto-create and replace semantics as the REST API.</p>
292
+ </div>
293
+ </div>
294
+
257
295
  <div class="ck-api-tabs__panel">
258
296
  <h2 class="ck-section-title">Provider Credentials</h2>
259
297
  <p class="ck-copy">LLM provider API keys. The <code>api_key</code> field is write-only and never returned in responses.</p>
@@ -21,8 +21,27 @@
21
21
  <%= form.text_area :csv_data, rows: 12, class: "ck-input ck-input--area ck-input--code", placeholder: "content,audience\nFirst ticket text,internal\nSecond ticket text,customer" %>
22
22
  </div>
23
23
 
24
+ <%= render "completion_kit/tags/picker", record: dataset, param_namespace: :dataset %>
25
+
24
26
  <div class="ck-actions">
25
- <%= link_to "Cancel", datasets_path, class: ck_button_classes(:light, variant: :outline) %>
27
+ <% if dataset.persisted? %>
28
+ <% runs_n = dataset.runs.count %>
29
+ <% responses_n = CompletionKit::Response.where(run_id: dataset.runs.select(:id)).count %>
30
+ <% confirm = if runs_n.zero?
31
+ "Delete \"#{dataset.name}\"? It has no runs."
32
+ else
33
+ "Delete \"#{dataset.name}\"? Cascades through #{pluralize(runs_n, 'run')} and #{pluralize(responses_n, 'response')} (and their reviews) that used this dataset."
34
+ end %>
35
+ <%= button_to dataset_path(dataset), method: :delete,
36
+ form_class: "inline-block",
37
+ class: "ck-icon-btn",
38
+ title: "Delete dataset",
39
+ "aria-label": "Delete dataset",
40
+ data: { turbo_confirm: confirm } do %>
41
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
42
+ <% end %>
43
+ <% end %>
44
+ <%= link_to "Cancel", datasets_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
26
45
  <%= form.submit(dataset.persisted? ? "Save dataset" : "Create dataset", class: ck_button_classes(:dark)) %>
27
46
  </div>
28
47
  </div>
@@ -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: datasets_path %>
15
+
11
16
  <% if @datasets.any? %>
12
17
  <table class="ck-results-table">
13
18
  <thead>
@@ -22,7 +27,14 @@
22
27
  <tbody>
23
28
  <% @datasets.each do |dataset| %>
24
29
  <tr onclick="window.location='<%= dataset_path(dataset) %>'" style="cursor: pointer;">
25
- <td><strong><%= dataset.name %></strong></td>
30
+ <td>
31
+ <strong><%= dataset.name %></strong>
32
+ <% if dataset.tags.any? %>
33
+ <div class="tag-marks-row">
34
+ <%= render "completion_kit/tags/marks", tags: dataset.tags %>
35
+ </div>
36
+ <% end %>
37
+ </td>
26
38
  <td><%= dataset.row_count %></td>
27
39
  <td><%= dataset.runs.count %></td>
28
40
  <td class="ck-meta-copy"><time datetime="<%= dataset.created_at.iso8601 %>"><%= dataset.created_at.strftime("%b %-d, %Y") %></time></td>
@@ -31,6 +43,10 @@
31
43
  <% end %>
32
44
  </tbody>
33
45
  </table>
46
+ <% elsif @selected_tags.any? %>
47
+ <div class="ck-empty">
48
+ <p>No datasets match these tags. <%= link_to "Clear filters", datasets_path, class: "ck-link" %>.</p>
49
+ </div>
34
50
  <% else %>
35
51
  <div class="ck-empty">
36
52
  No datasets yet. Create one to use with your prompts.
@@ -12,6 +12,12 @@
12
12
  </div>
13
13
  </section>
14
14
 
15
+ <% if @dataset.tags.any? %>
16
+ <div class="tag-marks-row tag-marks-row--header">
17
+ <%= render "completion_kit/tags/marks", tags: @dataset.tags %>
18
+ </div>
19
+ <% end %>
20
+
15
21
  <section>
16
22
  <p class="ck-kicker">CSV preview</p>
17
23
  <%
@@ -1,19 +1,17 @@
1
1
  <%= form_with(model: metric_group, url: metric_group.persisted? ? metric_group_path(metric_group) : metric_groups_path, local: true) do |form| %>
2
- <% if metric_group.errors.any? %>
3
- <div class="ck-flash ck-flash--alert">
4
- <p class="ck-flash__title"><%= pluralize(metric_group.errors.count, "problem") %> prevented this metric group from being saved.</p>
5
- <ul class="ck-error-list">
6
- <% metric_group.errors.full_messages.each do |message| %>
7
- <li><%= message %></li>
8
- <% end %>
9
- </ul>
10
- </div>
11
- <% end %>
2
+ <% name_error = metric_group.errors[:name].first %>
12
3
 
13
4
  <div class="ck-card ck-form-card">
14
5
  <div class="ck-field">
15
6
  <%= form.label :name, "Metric group name", class: "ck-label" %>
16
- <%= form.text_field :name, class: "ck-input", placeholder: "Support quality" %>
7
+ <%= form.text_field :name,
8
+ class: ["ck-input", ("ck-input--error" if name_error)].compact.join(" "),
9
+ placeholder: "Support quality",
10
+ autofocus: !metric_group.persisted?,
11
+ "aria-invalid": name_error ? "true" : nil %>
12
+ <% if name_error %>
13
+ <p class="ck-field-error" role="alert"><%= name_error %></p>
14
+ <% end %>
17
15
  </div>
18
16
 
19
17
  <div class="ck-field">
@@ -21,16 +19,41 @@
21
19
  <%= form.text_area :description, rows: 3, class: "ck-input ck-input--area", placeholder: "When this metric group should be used." %>
22
20
  </div>
23
21
 
22
+ <%= render "completion_kit/tags/picker", record: metric_group, param_namespace: :metric_group %>
23
+
24
24
  <div class="ck-field">
25
25
  <p class="ck-label">Metrics in this group</p>
26
- <p class="ck-hint">Pick the metrics to include.</p>
27
- <div class="ck-list ck-list--compact">
26
+ <p class="ck-hint">Pick the metrics to include. Filter the list by tag to narrow it down.</p>
27
+
28
+ <% available_filter_tags = @metrics.flat_map(&:tags).uniq.sort_by(&:name) %>
29
+ <% if available_filter_tags.any? %>
30
+ <div class="ck-metric-tag-filter" data-metric-tag-filter>
31
+ <span class="ck-metric-tag-filter__label">Filter metrics by tag</span>
32
+ <% available_filter_tags.each do |tag| %>
33
+ <button type="button"
34
+ class="tag-mark tag-mark--off ck-metric-tag-filter__chip"
35
+ style="--mark-color: var(--tag-<%= tag.color %>);"
36
+ data-filter-tag="<%= tag.name %>"
37
+ onclick="ckToggleMetricGroupFilter(this)"><%= tag.name %></button>
38
+ <% end %>
39
+ </div>
40
+ <% end %>
41
+
42
+ <div class="ck-metric-checkboxes" data-metric-checkboxes>
28
43
  <% @metrics.each do |metric| %>
29
- <label class="ck-item">
30
- <%= check_box_tag "metric_group[metric_ids][]", metric.id, metric_group.metrics.exists?(metric.id), class: "ck-checkbox" %>
31
- <span>
32
- <strong><%= metric.name %></strong>
33
- <span class="ck-meta-copy"><%= metric.instruction.presence || "No instruction set." %></span>
44
+ <label class="ck-checkbox-label" data-metric-tags="<%= metric.tag_names.join(",") %>">
45
+ <%= check_box_tag "metric_group[metric_ids][]", metric.id, metric_group.metrics.exists?(metric.id), class: "ck-checkbox", id: "metric_group_metric_#{metric.id}" %>
46
+ <span class="ck-checkbox-label__box" aria-hidden="true"></span>
47
+ <span class="ck-checkbox-label__body">
48
+ <span class="ck-checkbox-label__text"><%= metric.name %></span>
49
+ <% if metric.instruction.present? %>
50
+ <span class="ck-checkbox-label__hint"><%= truncate(metric.instruction.to_s, length: 90) %></span>
51
+ <% end %>
52
+ <% if metric.tags.any? %>
53
+ <div class="tag-marks-row">
54
+ <%= render "completion_kit/tags/marks", tags: metric.tags %>
55
+ </div>
56
+ <% end %>
34
57
  </span>
35
58
  </label>
36
59
  <% end %>
@@ -38,8 +61,40 @@
38
61
  <%= hidden_field_tag "metric_group[metric_ids][]", "" %>
39
62
  </div>
40
63
 
64
+ <script>
65
+ function ckToggleMetricGroupFilter(btn) {
66
+ btn.classList.toggle("tag-mark--off");
67
+ var bar = btn.closest("[data-metric-tag-filter]");
68
+ var grid = bar && bar.parentElement.querySelector("[data-metric-checkboxes]");
69
+ if (!grid) return;
70
+ var active = Array.prototype.map.call(
71
+ bar.querySelectorAll(".ck-metric-tag-filter__chip:not(.tag-mark--off)"),
72
+ function(b) { return b.getAttribute("data-filter-tag"); }
73
+ );
74
+ grid.querySelectorAll(".ck-checkbox-label").forEach(function(lbl) {
75
+ var tags = (lbl.getAttribute("data-metric-tags") || "").split(",").filter(Boolean);
76
+ var visible = active.length === 0 || tags.some(function(t) { return active.indexOf(t) !== -1; });
77
+ lbl.style.display = visible ? "" : "none";
78
+ });
79
+ }
80
+ </script>
81
+
41
82
  <div class="ck-actions">
42
- <%= link_to "Cancel", metrics_path, class: ck_button_classes(:light, variant: :outline) %>
83
+ <% if metric_group.persisted? %>
84
+ <% members_n = metric_group.metrics.count %>
85
+ <% confirm = members_n.zero? ?
86
+ "Delete \"#{metric_group.name}\"? It has no members." :
87
+ "Delete \"#{metric_group.name}\"? Removes this grouping. #{pluralize(members_n, 'member metric')} #{members_n == 1 ? 'is' : 'are'} kept." %>
88
+ <%= button_to metric_group_path(metric_group), method: :delete,
89
+ form_class: "inline-block",
90
+ class: "ck-icon-btn",
91
+ title: "Delete metric group",
92
+ "aria-label": "Delete metric group",
93
+ data: { turbo_confirm: confirm } do %>
94
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
95
+ <% end %>
96
+ <% end %>
97
+ <%= link_to "Cancel", metrics_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
43
98
  <%= form.submit(metric_group.persisted? ? "Save metric group" : "Create metric group", class: ck_button_classes(:dark)) %>
44
99
  </div>
45
100
  </div>
@@ -13,27 +13,53 @@
13
13
  </div>
14
14
  </section>
15
15
 
16
+ <%= render "completion_kit/tags/filter_bar",
17
+ available: @available_tags,
18
+ selected: @selected_tags,
19
+ base_path: metric_groups_path %>
20
+
16
21
  <% if @metric_groups.any? %>
17
22
  <table class="ck-results-table">
18
23
  <thead>
19
24
  <tr>
20
25
  <th>Name</th>
21
26
  <th>Description</th>
22
- <th>Metrics</th>
27
+ <th>Members</th>
23
28
  <th></th>
24
29
  </tr>
25
30
  </thead>
26
31
  <tbody>
27
32
  <% @metric_groups.each do |metric_group| %>
28
33
  <tr onclick="window.location='<%= metric_group_path(metric_group) %>'" style="cursor: pointer;">
29
- <td><strong><%= metric_group.name %></strong></td>
30
- <td class="ck-meta-copy"><%= truncate(metric_group.description.to_s, length: 90).presence || "—" %></td>
31
- <td class="ck-meta-copy"><%= metric_group.metrics.any? ? metric_group.metrics.map(&:name).join(", ") : "empty" %></td>
34
+ <td>
35
+ <strong><%= metric_group.name %></strong>
36
+ <% if metric_group.tags.any? %>
37
+ <div class="tag-marks-row">
38
+ <%= render "completion_kit/tags/marks", tags: metric_group.tags %>
39
+ </div>
40
+ <% end %>
41
+ </td>
42
+ <td class="ck-meta-copy"><div class="ck-clamp-2"><%= metric_group.description.presence || "—" %></div></td>
43
+ <td>
44
+ <% if metric_group.metrics.any? %>
45
+ <div class="ck-mg-members">
46
+ <% metric_group.metrics.each do |m| %>
47
+ <span class="ck-mg-member"><%= m.name %></span>
48
+ <% end %>
49
+ </div>
50
+ <% else %>
51
+ <span class="ck-metrics-table__dim">empty</span>
52
+ <% end %>
53
+ </td>
32
54
  <td class="ck-results-table__arrow">&rarr;</td>
33
55
  </tr>
34
56
  <% end %>
35
57
  </tbody>
36
58
  </table>
59
+ <% elsif @selected_tags.any? %>
60
+ <div class="ck-empty">
61
+ <p>No metric groups match these tags. <%= link_to "Clear filters", metric_groups_path, class: "ck-link" %>.</p>
62
+ </div>
37
63
  <% else %>
38
64
  <div class="ck-empty">
39
65
  <p>No metric groups yet. <%= link_to "Create one", new_metric_group_path, class: "ck-link" %> if you want to group multiple metrics and apply them together.</p>
@@ -43,8 +43,26 @@
43
43
  </div>
44
44
  </div>
45
45
 
46
+ <%= render "completion_kit/tags/picker", record: metric, param_namespace: :metric %>
47
+
46
48
  <div class="ck-actions">
47
- <%= link_to "Cancel", metrics_path, class: ck_button_classes(:light, variant: :outline) %>
49
+ <% if metric.persisted? %>
50
+ <% groups_n = metric.metric_groups.count %>
51
+ <% reviews_n = metric.reviews.count %>
52
+ <% parts = [] %>
53
+ <% parts << "in #{pluralize(groups_n, 'metric group')} (removed from each)" if groups_n > 0 %>
54
+ <% parts << "scored in #{pluralize(reviews_n, 'review')} (scores kept, link cleared)" if reviews_n > 0 %>
55
+ <% confirm = parts.empty? ? "Delete \"#{metric.name}\"? It's not in use." : "Delete \"#{metric.name}\"? It's #{parts.to_sentence}." %>
56
+ <%= button_to metric_path(metric), method: :delete,
57
+ form_class: "inline-block",
58
+ class: "ck-icon-btn",
59
+ title: "Delete metric",
60
+ "aria-label": "Delete metric",
61
+ data: { turbo_confirm: confirm } do %>
62
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
63
+ <% end %>
64
+ <% end %>
65
+ <%= link_to "Cancel", metrics_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
48
66
  <%= form.submit(metric.persisted? ? "Save metric" : "Create metric", class: ck_button_classes(:dark)) %>
49
67
  </div>
50
68
  </div>
@@ -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: metrics_path %>
15
+
11
16
  <% if @metrics.any? %>
12
17
  <table class="ck-results-table ck-metrics-table">
13
18
  <thead>
@@ -21,8 +26,15 @@
21
26
  <tbody>
22
27
  <% @metrics.each do |metric| %>
23
28
  <tr onclick="window.location='<%= metric_path(metric) %>'" style="cursor: pointer;">
24
- <td><strong><%= metric.name %></strong></td>
25
- <td class="ck-meta-copy"><%= truncate(metric.instruction.to_s, length: 90).presence || "—" %></td>
29
+ <td>
30
+ <strong><%= metric.name %></strong>
31
+ <% if metric.tags.any? %>
32
+ <div class="tag-marks-row">
33
+ <%= render "completion_kit/tags/marks", tags: metric.tags %>
34
+ </div>
35
+ <% end %>
36
+ </td>
37
+ <td class="ck-meta-copy"><div class="ck-clamp-2"><%= metric.instruction.presence || "—" %></div></td>
26
38
  <td>
27
39
  <% groups = metric.metric_groups %>
28
40
  <% if groups.any? %>
@@ -48,6 +60,10 @@
48
60
  Use the same metrics on multiple runs? <%= link_to "Group them →", metric_groups_path, class: "ck-link" %>
49
61
  </p>
50
62
  <% end %>
63
+ <% elsif @selected_tags.any? %>
64
+ <div class="ck-empty">
65
+ <p>No metrics match these tags. <%= link_to "Clear filters", metrics_path, class: "ck-link" %>.</p>
66
+ </div>
51
67
  <% else %>
52
68
  <div class="ck-empty">
53
69
  <p>No metrics yet. <%= link_to "Create your first metric", new_metric_path, class: "ck-link" %> to start scoring prompt outputs.</p>
@@ -12,6 +12,12 @@
12
12
  </div>
13
13
  </section>
14
14
 
15
+ <% if @metric.tags.any? %>
16
+ <div class="tag-marks-row tag-marks-row--header">
17
+ <%= render "completion_kit/tags/marks", tags: @metric.tags %>
18
+ </div>
19
+ <% end %>
20
+
15
21
  <% if @metric.instruction.present? %>
16
22
  <section class="ck-card">
17
23
  <p class="ck-kicker">Instruction</p>
@@ -47,8 +47,27 @@
47
47
  <p class="ck-field-hint" id="refresh-status" style="min-height: 1.2em; margin-top: -0.25rem; font-size: 0.75rem;">&nbsp;</p>
48
48
  </div>
49
49
 
50
+ <%= render "completion_kit/tags/picker", record: prompt, param_namespace: :prompt %>
51
+
50
52
  <div class="ck-actions">
51
- <%= link_to "Cancel", prompts_path, class: ck_button_classes(:light, variant: :outline) %>
53
+ <% if prompt.persisted? %>
54
+ <% runs_n = prompt.runs.count %>
55
+ <% responses_n = prompt.responses.count %>
56
+ <% confirm = if runs_n.zero?
57
+ "Delete \"#{prompt.display_name}\"? This version has no runs."
58
+ else
59
+ "Delete \"#{prompt.display_name}\"? Cascades through #{pluralize(runs_n, 'run')} and #{pluralize(responses_n, 'response')} (and their reviews). Other versions of this prompt are untouched."
60
+ end %>
61
+ <%= button_to prompt_path(prompt), method: :delete,
62
+ form_class: "inline-block",
63
+ class: "ck-icon-btn",
64
+ title: "Delete prompt",
65
+ "aria-label": "Delete prompt",
66
+ data: { turbo_confirm: confirm } do %>
67
+ <%= heroicon_tag "trash", variant: :outline, size: 16, "aria-hidden": "true" %>
68
+ <% end %>
69
+ <% end %>
70
+ <%= link_to "Cancel", prompts_path, class: ck_button_classes(:light, variant: :outline), tabindex: "0" %>
52
71
  <%= form.submit(prompt.persisted? ? "Save prompt" : "Create prompt", class: ck_button_classes(:dark), disabled: available.empty?) %>
53
72
  </div>
54
73
  </div>
@@ -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: prompts_path %>
15
+
11
16
  <% if @prompts.any? %>
12
17
  <table class="ck-results-table ck-prompts-table">
13
18
  <thead>
@@ -23,7 +28,14 @@
23
28
  <tbody>
24
29
  <% @prompts.each do |prompt| %>
25
30
  <tr onclick="window.location='<%= prompt_path(prompt) %>'" style="cursor: pointer;">
26
- <td><strong><%= prompt.name %></strong></td>
31
+ <td>
32
+ <strong><%= prompt.name %></strong>
33
+ <% if prompt.tags.any? %>
34
+ <div class="tag-marks-row">
35
+ <%= render "completion_kit/tags/marks", tags: prompt.tags %>
36
+ </div>
37
+ <% end %>
38
+ </td>
27
39
  <% latest_version = prompt.family_versions.maximum(:version_number) %>
28
40
  <td>
29
41
  <span class="ck-chip ck-chip--soft"><%= prompt.version_label %></span>
@@ -58,6 +70,10 @@
58
70
  <% end %>
59
71
  </tbody>
60
72
  </table>
73
+ <% elsif @selected_tags.any? %>
74
+ <div class="ck-empty">
75
+ <p>No prompts match these tags. <%= link_to "Clear filters", prompts_path, class: "ck-link" %>.</p>
76
+ </div>
61
77
  <% else %>
62
78
  <div class="ck-empty">
63
79
  No prompts yet. Create one to get started.
@@ -25,6 +25,12 @@
25
25
  </div>
26
26
  </section>
27
27
 
28
+ <% if @prompt.tags.any? %>
29
+ <div class="tag-marks-row tag-marks-row--header">
30
+ <%= render "completion_kit/tags/marks", tags: @prompt.tags %>
31
+ </div>
32
+ <% end %>
33
+
28
34
  <section>
29
35
  <div class="ck-prompt-preview__header">
30
36
  <p class="ck-kicker">Prompt</p>