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
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c2de74d874b4b8ea5952f574bd922360399b2287d0cf87a3ef05bf11e4c50cd
4
- data.tar.gz: 3a949c1db4c91dd7d2b3ef353ac50a4c3a0d16210695adbd9b4e98a7c43157c1
3
+ metadata.gz: 89766aed83665a19253ddf7f4fcbc99ad3e9bc8c17123d7f7d33acac36bf3096
4
+ data.tar.gz: fe71adba78f455a2ce36ef3649bb3e0c94ca77f7534fbe6377604bf6f66d36ab
5
5
  SHA512:
6
- metadata.gz: 4212a7bef6dbf0302631e4a70cbe9823656687ecee26003aaa940c10aff39c81c94402b2dbb9820259b11bde7ce6bcb22662b4749a9a8631884635fd88df18f9
7
- data.tar.gz: 7cba484ac695ebeaa5956de196208bfa6c68d51d47e46726d2804a12db5df16f82acd9632bab3aac5538bce4a5bc977e65b5137d4dea214c0584541d03d50c8f
6
+ metadata.gz: 7dc10c0b796286e5c4535c761da1af4d49d4b1011477167849c8a398644cec30d310534971df8515ff5cda957596589d1277e6942c569b5c943a575a9c2a4e6c
7
+ data.tar.gz: a5a17559fc2032473aa6dd2946948cefd5b195f73dbd48e68c8ead4e322a469a1faca74e170168dad69dd947a88f2687675c007b41a92a91afbacaae752bc50d
data/README.md CHANGED
@@ -64,6 +64,17 @@ bin/rails db:migrate
64
64
 
65
65
  The engine mounts at `/completion_kit` in your app. CompletionKit's generate and judge flows enqueue Active Job jobs (`CompletionKit::GenerateRowJob`, `CompletionKit::JudgeReviewJob`, `CompletionKit::RunCompletionCheckJob`), so your host app needs an Active Job adapter that actually processes them — Solid Queue, Sidekiq, GoodJob, etc. The `:async` adapter is **not** suitable for production: it runs jobs in the web Puma's thread pool with no durability and no retry, and a long LLM call will block request handling.
66
66
 
67
+ ### Host-app layout integration
68
+
69
+ If your host app overrides the engine layout (e.g. `layout "application"` on engine controllers, or rendering engine views inside your own shell), include both the engine's stylesheet and JavaScript in that layout:
70
+
71
+ ```erb
72
+ <%= stylesheet_link_tag "completion_kit/application", media: "all" %>
73
+ <%= javascript_include_tag "completion_kit/application", defer: true %>
74
+ ```
75
+
76
+ Without the JavaScript include, in-page behaviours silently fail: live tag-breadcrumb updates, relative-time ticking, CSV row hover-expand, model-refresh progress, focus-first-error, and local-time formatting.
77
+
67
78
  ## Providers
68
79
 
69
80
  CompletionKit discovers available models from each provider's API automatically.
@@ -136,6 +147,7 @@ Only one mode can be active.
136
147
  - **Response.** The model's output for one dataset row, with reviews attached.
137
148
  - **Metric.** An evaluation dimension with a name, instruction, evaluation steps, and a 1-5 star scoring scale. The LLM judge uses this to score each response.
138
149
  - **Metric Group.** A reusable group of metrics you can apply to a run as a set.
150
+ - **Tag.** A domain label you can attach to prompts, runs, metrics, and datasets. Auto-assigned from a 10-color palette. Filter any index page by tag (`?tag[]=...`).
139
151
  - **Provider Credential.** An API key for a model provider. Encrypted at rest, never returned through the API.
140
152
 
141
153
  ## REST API
@@ -1 +1,2 @@
1
1
  //= link_directory ../stylesheets/completion_kit .css
2
+ //= link_directory ../javascripts/completion_kit .js
@@ -0,0 +1,157 @@
1
+ document.addEventListener("turbo:load", function() {
2
+ document.querySelectorAll("[data-local-time]").forEach(function(el) {
3
+ var d = new Date(el.getAttribute("datetime"));
4
+ el.textContent = d.toLocaleString(undefined, {year:"numeric",month:"short",day:"numeric",hour:"2-digit",minute:"2-digit"});
5
+ });
6
+ ckTickRelativeTimes();
7
+ ckAutoFocusFirstError();
8
+ });
9
+
10
+ document.addEventListener("input", function(e) {
11
+ if (!e.target || e.target.id !== "tag_name") return;
12
+ var text = document.getElementById("tag-pill-text");
13
+ if (!text) return;
14
+ var v = e.target.value.trim().toLowerCase();
15
+ text.textContent = v.length ? v : (text.dataset.placeholder || "");
16
+ });
17
+
18
+ function ckAutoFocusFirstError() {
19
+ var fieldSelector = "input:not([type=hidden]):not([type=submit]):not([type=button]):not([type=reset]):not([type=file]), textarea, select";
20
+ var marker = document.querySelector("form .ck-flash--alert, form [aria-invalid='true'], form .ck-field-error");
21
+
22
+ var target;
23
+ if (marker) {
24
+ var form = marker.closest("form");
25
+ target = form && (form.querySelector("[aria-invalid='true']") || form.querySelector(fieldSelector));
26
+ } else if (/\/new(\/|$)/.test(window.location.pathname)) {
27
+ target = document.querySelector("form " + fieldSelector);
28
+ }
29
+
30
+ if (!target || typeof target.focus !== "function") return;
31
+ target.focus({ preventScroll: false });
32
+ if (typeof target.setSelectionRange === "function" && typeof target.value === "string") {
33
+ try { target.setSelectionRange(target.value.length, target.value.length); } catch (e) {}
34
+ }
35
+ }
36
+
37
+ function ckRelativeTime(then) {
38
+ var seconds = Math.round((Date.now() - then.getTime()) / 1000);
39
+ if (seconds < 5) return "just now";
40
+ if (seconds < 60) return "less than a minute";
41
+ var minutes = Math.round(seconds / 60);
42
+ if (minutes < 60) return minutes === 1 ? "1 minute" : minutes + " minutes";
43
+ var hours = Math.round(minutes / 60);
44
+ if (hours < 24) return hours === 1 ? "about 1 hour" : "about " + hours + " hours";
45
+ var days = Math.round(hours / 24);
46
+ if (days < 30) return days === 1 ? "1 day" : days + " days";
47
+ var months = Math.round(days / 30);
48
+ if (months < 12) return months === 1 ? "about 1 month" : "about " + months + " months";
49
+ var years = Math.round(days / 365);
50
+ return years === 1 ? "about 1 year" : "about " + years + " years";
51
+ }
52
+
53
+ function ckRelativeTimeCompact(then) {
54
+ var seconds = Math.round((Date.now() - then.getTime()) / 1000);
55
+ if (seconds < 60) return "now";
56
+ var minutes = Math.round(seconds / 60);
57
+ if (minutes < 60) return minutes + "m";
58
+ var hours = Math.round(minutes / 60);
59
+ if (hours < 24) return hours + "h";
60
+ var days = Math.round(hours / 24);
61
+ if (days < 30) return days + "d";
62
+ var months = Math.round(days / 30);
63
+ if (months < 12) return months + "mo";
64
+ var years = Math.round(days / 365);
65
+ return years + "y";
66
+ }
67
+
68
+ function ckTickRelativeTimes() {
69
+ document.querySelectorAll("[data-relative-time]").forEach(function(el) {
70
+ var then = new Date(el.getAttribute("datetime"));
71
+ if (isNaN(then.getTime())) return;
72
+ var verbose = el.getAttribute("data-relative-time") === "verbose";
73
+ el.textContent = verbose ? ckRelativeTime(then) : ckRelativeTimeCompact(then);
74
+ el.setAttribute("title", then.toLocaleString());
75
+ });
76
+ }
77
+
78
+ if (!window.ckRelativeTimeInterval) {
79
+ window.ckRelativeTimeInterval = setInterval(ckTickRelativeTimes, 30000);
80
+ }
81
+ document.addEventListener("turbo:before-stream-render", function() {
82
+ requestAnimationFrame(ckTickRelativeTimes);
83
+ });
84
+
85
+ var ckCsvHoverTimer = null;
86
+ var ckCsvHoverRow = null;
87
+ document.addEventListener("mouseover", function(e) {
88
+ var row = e.target.closest && e.target.closest(".ck-csv-table tbody tr");
89
+ if (!row || row === ckCsvHoverRow) return;
90
+ if (ckCsvHoverRow) ckCsvHoverRow.classList.remove("ck-csv-row--expanded");
91
+ ckCsvHoverRow = row;
92
+ clearTimeout(ckCsvHoverTimer);
93
+ ckCsvHoverTimer = setTimeout(function() {
94
+ if (ckCsvHoverRow === row) row.classList.add("ck-csv-row--expanded");
95
+ }, 350);
96
+ });
97
+ document.addEventListener("mouseout", function(e) {
98
+ var row = e.target.closest && e.target.closest(".ck-csv-table tbody tr");
99
+ if (!row) return;
100
+ var related = e.relatedTarget && e.relatedTarget.closest && e.relatedTarget.closest(".ck-csv-table tbody tr");
101
+ if (related === row) return;
102
+ clearTimeout(ckCsvHoverTimer);
103
+ row.classList.remove("ck-csv-row--expanded");
104
+ if (ckCsvHoverRow === row) ckCsvHoverRow = null;
105
+ });
106
+
107
+ var ckRefreshing = false;
108
+ function ckRefreshModels() {
109
+ if (ckRefreshing) return;
110
+ ckRefreshing = true;
111
+ var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
112
+ if (btn) btn.classList.add('ck-icon-btn--spinning');
113
+ ckUpdateRefreshProgress();
114
+ var csrfToken = document.querySelector('meta[name="csrf-token"]').getAttribute("content");
115
+ fetch("/completion_kit/refresh_models", {
116
+ method: "POST",
117
+ headers: { "X-CSRF-Token": csrfToken }
118
+ });
119
+ }
120
+
121
+ function ckUpdateRefreshProgress() {
122
+ var status = document.getElementById('refresh-status');
123
+ if (!status) return;
124
+ var carriers = document.querySelectorAll('[data-refresh-progress-carriers] [id^="discovery_status_"]');
125
+ var totalCurrent = 0, totalTotal = 0, anyDiscovering = false;
126
+ carriers.forEach(function(node) {
127
+ if (!node.querySelector('.ck-discovery-bar')) return;
128
+ if (node.querySelector('.ck-discovery-bar--failed') || node.querySelector('.ck-discovery-bar--completed')) return;
129
+ anyDiscovering = true;
130
+ var match = node.textContent.match(/(\d+)\s*\/\s*(\d+)/);
131
+ if (match) {
132
+ totalCurrent += parseInt(match[1], 10);
133
+ totalTotal += parseInt(match[2], 10);
134
+ }
135
+ });
136
+ if (anyDiscovering || ckRefreshing) {
137
+ if (totalTotal > 0) {
138
+ status.textContent = 'Refreshing models… ' + totalCurrent + '/' + totalTotal;
139
+ } else {
140
+ status.textContent = 'Refreshing models…';
141
+ }
142
+ }
143
+ }
144
+
145
+ document.addEventListener("turbo:before-stream-render", function(event) {
146
+ var target = event.target.getAttribute("target");
147
+ if (target && target.indexOf("discovery_status_") === 0) {
148
+ requestAnimationFrame(ckUpdateRefreshProgress);
149
+ }
150
+ if (target === "prompt_llm_model" || target === "run_judge_model") {
151
+ ckRefreshing = false;
152
+ var btn = document.querySelector('.ck-icon-btn[title="Refresh models"]');
153
+ if (btn) btn.classList.remove('ck-icon-btn--spinning');
154
+ var status = document.getElementById('refresh-status');
155
+ if (status) { status.textContent = 'Models updated.'; setTimeout(function() { status.textContent = ' '; }, 3000); }
156
+ }
157
+ });
@@ -501,6 +501,11 @@ tr:hover .ck-chip--publish {
501
501
  transform: translateY(-1px);
502
502
  }
503
503
 
504
+ .ck-button:focus-visible {
505
+ outline: 2px solid var(--ck-accent);
506
+ outline-offset: 2px;
507
+ }
508
+
504
509
  .ck-button--primary {
505
510
  background: var(--ck-accent);
506
511
  color: #080b14;
@@ -1531,6 +1536,11 @@ tr:hover .ck-chip--publish {
1531
1536
  color: var(--ck-danger);
1532
1537
  }
1533
1538
 
1539
+ .ck-icon-btn:focus-visible {
1540
+ outline: 2px solid var(--ck-accent);
1541
+ outline-offset: 2px;
1542
+ }
1543
+
1534
1544
  .ck-icon-btn--spinning svg {
1535
1545
  animation: ck-spin 0.8s linear infinite;
1536
1546
  }
@@ -2594,6 +2604,7 @@ select.ck-input {
2594
2604
  align-items: center;
2595
2605
  gap: 0.4rem;
2596
2606
  padding: 0.32rem 0.7rem;
2607
+ white-space: nowrap;
2597
2608
  background: transparent;
2598
2609
  border: 1px solid var(--ck-line);
2599
2610
  border-radius: 999px;
@@ -2842,6 +2853,72 @@ select.ck-input {
2842
2853
  color: var(--ck-dim);
2843
2854
  }
2844
2855
 
2856
+ .ck-clamp-2 {
2857
+ display: -webkit-box;
2858
+ -webkit-line-clamp: 2;
2859
+ -webkit-box-orient: vertical;
2860
+ overflow: hidden;
2861
+ }
2862
+
2863
+ .ck-tags-table th:first-child,
2864
+ .ck-tags-table td:first-child {
2865
+ width: 35%;
2866
+ }
2867
+ .ck-tags-table th:nth-child(2),
2868
+ .ck-tags-table td:nth-child(2) {
2869
+ width: 60%;
2870
+ }
2871
+ .tag-mark.tag-mark--lg {
2872
+ padding: 4px 10px;
2873
+ font-size: 0.8rem;
2874
+ letter-spacing: 0.02em;
2875
+ border-radius: 5px;
2876
+ }
2877
+ .ck-tags-table__unused {
2878
+ color: var(--ck-dim);
2879
+ font-style: italic;
2880
+ }
2881
+
2882
+ .ck-mg-members {
2883
+ display: flex;
2884
+ flex-wrap: wrap;
2885
+ gap: 0.3rem;
2886
+ }
2887
+ .ck-mg-member {
2888
+ display: inline-flex;
2889
+ align-items: center;
2890
+ padding: 2px 8px;
2891
+ background: var(--ck-surface-soft);
2892
+ border: 1px solid var(--ck-line);
2893
+ border-radius: 4px;
2894
+ font-family: var(--ck-mono);
2895
+ font-size: 11px;
2896
+ letter-spacing: 0.02em;
2897
+ color: var(--ck-muted);
2898
+ white-space: nowrap;
2899
+ }
2900
+
2901
+ .ck-metric-tag-filter {
2902
+ display: flex;
2903
+ flex-wrap: wrap;
2904
+ align-items: center;
2905
+ gap: 8px;
2906
+ margin: 0.5rem 0 0.85rem;
2907
+ }
2908
+ .ck-metric-tag-filter__label {
2909
+ font-family: var(--ck-mono);
2910
+ font-size: 11px;
2911
+ letter-spacing: 0.14em;
2912
+ text-transform: uppercase;
2913
+ color: var(--ck-dim);
2914
+ margin-right: 0.3rem;
2915
+ }
2916
+ .ck-metric-tag-filter__chip {
2917
+ border-radius: 5px;
2918
+ font: inherit;
2919
+ cursor: pointer;
2920
+ }
2921
+
2845
2922
  .ck-metrics-table__groups {
2846
2923
  display: flex;
2847
2924
  flex-wrap: wrap;
@@ -2909,6 +2986,10 @@ a.ck-metric-group-pill {
2909
2986
  white-space: nowrap;
2910
2987
  }
2911
2988
 
2989
+ .ck-runs-table__identity .tag-marks-row {
2990
+ padding-left: 1.2rem;
2991
+ }
2992
+
2912
2993
  .ck-runs-table__config-link {
2913
2994
  color: var(--ck-muted);
2914
2995
  text-decoration: none;
@@ -3396,3 +3477,304 @@ a.ck-metric-group-pill {
3396
3477
  text-overflow: ellipsis;
3397
3478
  }
3398
3479
 
3480
+ :root {
3481
+ --tag-crimson: #F24E1E;
3482
+ --tag-burnt-orange: #FF8A00;
3483
+ --tag-amber: #FFC700;
3484
+ --tag-mint: #14AE5C;
3485
+ --tag-deep-emerald: #00623F;
3486
+ --tag-electric-cyan: #00D1FF;
3487
+ --tag-cobalt-blue: #0D99FF;
3488
+ --tag-deep-indigo: #5551FF;
3489
+ --tag-amethyst: #9747FF;
3490
+ --tag-rose: #FF5CBE;
3491
+ }
3492
+
3493
+ .tag {
3494
+ display: inline-flex;
3495
+ align-items: center;
3496
+ gap: 6px;
3497
+ padding: 4px 9px;
3498
+ min-height: calc(1.4em + 8px);
3499
+ border-radius: 5px;
3500
+ font-family: var(--ck-mono);
3501
+ font-size: 12.5px;
3502
+ font-weight: 500;
3503
+ letter-spacing: 0.01em;
3504
+ text-transform: lowercase;
3505
+ color: white;
3506
+ line-height: 1.4;
3507
+ white-space: nowrap;
3508
+ transition: filter 120ms ease;
3509
+ text-decoration: none;
3510
+ user-select: none;
3511
+ }
3512
+
3513
+ .tag::before {
3514
+ content: "";
3515
+ width: 6px;
3516
+ height: 6px;
3517
+ border-radius: 50%;
3518
+ background: rgba(255, 255, 255, 0.85);
3519
+ flex-shrink: 0;
3520
+ }
3521
+
3522
+ .tag > span:empty { display: none; }
3523
+
3524
+ a.tag, label.tag { cursor: pointer; }
3525
+ a.tag:hover, label.tag:hover { filter: brightness(1.12); }
3526
+
3527
+
3528
+ .tag-crimson { background-color: var(--tag-crimson); --current-tag-color: var(--tag-crimson); }
3529
+ .tag-burnt-orange { background-color: var(--tag-burnt-orange); --current-tag-color: var(--tag-burnt-orange); color: #2a1300; }
3530
+ .tag-burnt-orange::before { background: rgba(0, 0, 0, 0.55); }
3531
+ .tag-amber { background-color: var(--tag-amber); --current-tag-color: var(--tag-amber); color: #1a0f00; }
3532
+ .tag-amber::before { background: rgba(0, 0, 0, 0.55); }
3533
+ .tag-mint { background-color: var(--tag-mint); --current-tag-color: var(--tag-mint); }
3534
+ .tag-deep-emerald { background-color: var(--tag-deep-emerald); --current-tag-color: var(--tag-deep-emerald); }
3535
+ .tag-electric-cyan { background-color: var(--tag-electric-cyan); --current-tag-color: var(--tag-electric-cyan); color: #001620; }
3536
+ .tag-electric-cyan::before { background: rgba(0, 0, 0, 0.55); }
3537
+ .tag-cobalt-blue { background-color: var(--tag-cobalt-blue); --current-tag-color: var(--tag-cobalt-blue); }
3538
+ .tag-deep-indigo { background-color: var(--tag-deep-indigo); --current-tag-color: var(--tag-deep-indigo); }
3539
+ .tag-amethyst { background-color: var(--tag-amethyst); --current-tag-color: var(--tag-amethyst); }
3540
+ .tag-rose { background-color: var(--tag-rose); --current-tag-color: var(--tag-rose); color: #2a0017; }
3541
+ .tag-rose::before { background: rgba(0, 0, 0, 0.6); }
3542
+
3543
+ .tag-outline {
3544
+ background-color: transparent !important;
3545
+ border: 1px solid currentColor;
3546
+ box-shadow: none;
3547
+ }
3548
+ .tag-outline::before { background: currentColor; }
3549
+ .tag-outline.tag-crimson { color: var(--tag-crimson); }
3550
+ .tag-outline.tag-burnt-orange { color: var(--tag-burnt-orange); }
3551
+ .tag-outline.tag-amber { color: var(--tag-amber); }
3552
+ .tag-outline.tag-mint { color: var(--tag-mint); }
3553
+ .tag-outline.tag-deep-emerald { color: var(--tag-deep-emerald); }
3554
+ .tag-outline.tag-electric-cyan { color: var(--tag-electric-cyan); }
3555
+ .tag-outline.tag-cobalt-blue { color: var(--tag-cobalt-blue); }
3556
+ .tag-outline.tag-deep-indigo { color: var(--tag-deep-indigo); }
3557
+ .tag-outline.tag-amethyst { color: var(--tag-amethyst); }
3558
+ .tag-outline.tag-rose { color: var(--tag-rose); }
3559
+
3560
+ /* Inline mark — quiet "color dot + name" used on row metadata */
3561
+ .tag-mark {
3562
+ display: inline-flex;
3563
+ align-items: center;
3564
+ gap: 5px;
3565
+ padding: 2px 8px;
3566
+ min-height: 22px;
3567
+ box-sizing: border-box;
3568
+ border: 1px solid transparent;
3569
+ border-radius: 4px;
3570
+ font-family: var(--ck-mono);
3571
+ font-size: 11px;
3572
+ letter-spacing: 0.02em;
3573
+ text-transform: lowercase;
3574
+ white-space: nowrap;
3575
+ background: color-mix(in srgb, var(--mark-color) 24%, transparent);
3576
+ color: color-mix(in srgb, var(--mark-color) 88%, var(--ck-text));
3577
+ }
3578
+
3579
+ .tag-mark::before {
3580
+ content: "";
3581
+ display: inline-block;
3582
+ width: 0.95em;
3583
+ height: 0.95em;
3584
+ flex-shrink: 0;
3585
+ background-color: currentColor;
3586
+ -webkit-mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill-rule='evenodd' clip-rule='evenodd' d='M3 4.5C3 3.67 3.67 3 4.5 3h5.379c.398 0 .779.158 1.06.44L18.06 10.56c.585.586.585 1.535 0 2.121l-5.379 5.379a1.5 1.5 0 0 1-2.121 0L3.44 10.94A1.5 1.5 0 0 1 3 9.879V4.5Zm3.5 1.75a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z' fill='black'/></svg>") no-repeat center / contain;
3587
+ mask: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 20 20'><path fill-rule='evenodd' clip-rule='evenodd' d='M3 4.5C3 3.67 3.67 3 4.5 3h5.379c.398 0 .779.158 1.06.44L18.06 10.56c.585.586.585 1.535 0 2.121l-5.379 5.379a1.5 1.5 0 0 1-2.121 0L3.44 10.94A1.5 1.5 0 0 1 3 9.879V4.5Zm3.5 1.75a1.25 1.25 0 1 1 0 2.5 1.25 1.25 0 0 1 0-2.5Z' fill='black'/></svg>") no-repeat center / contain;
3588
+ }
3589
+ .tag-mark + .tag-mark { margin-left: 0.4rem; }
3590
+ .tag-marks-row {
3591
+ display: flex;
3592
+ flex-wrap: wrap;
3593
+ align-items: center;
3594
+ margin-top: 0.35rem;
3595
+ }
3596
+
3597
+ /* Picker — flat row of chips + inline input, no enclosing field */
3598
+ .ck-tag-picker {
3599
+ display: flex;
3600
+ flex-wrap: wrap;
3601
+ align-items: center;
3602
+ gap: 7px;
3603
+ }
3604
+ .ck-tag-picker__input {
3605
+ flex: 1;
3606
+ min-width: 160px;
3607
+ background: transparent;
3608
+ border: 0;
3609
+ padding: 2px 0;
3610
+ color: var(--ck-text);
3611
+ font-family: var(--ck-mono);
3612
+ font-size: 12.5px;
3613
+ letter-spacing: 0.01em;
3614
+ outline: none;
3615
+ }
3616
+ .ck-tag-picker__input::placeholder { color: var(--ck-dim); }
3617
+
3618
+ /* Picker chips: unchecked = neutral pill with a small tag-colored dot.
3619
+ Avoids looking "applied" simply because the tag color is on screen. */
3620
+ .ck-tag-picker label.tag-mark,
3621
+ .ck-tag-filter .tag-mark,
3622
+ .ck-metric-tag-filter .tag-mark {
3623
+ cursor: pointer;
3624
+ user-select: none;
3625
+ padding: 4px 10px;
3626
+ font-size: 0.8rem;
3627
+ transition: background 120ms ease, border-color 120ms ease, color 120ms ease;
3628
+ }
3629
+
3630
+ /* Selected state — picker, filter bars, metric-tag-filter share one look */
3631
+ .ck-tag-picker label.tag-mark:has(input:checked),
3632
+ .ck-tag-filter .tag-mark:not(.tag-mark--off),
3633
+ .ck-metric-tag-filter .tag-mark:not(.tag-mark--off) {
3634
+ background: color-mix(in srgb, var(--mark-color) 38%, transparent);
3635
+ color: color-mix(in srgb, var(--mark-color) 95%, var(--ck-text));
3636
+ border-color: color-mix(in srgb, var(--mark-color) 50%, transparent);
3637
+ }
3638
+
3639
+ /* Unselected state — same shape, neutral outline + muted text */
3640
+ .ck-tag-picker label.tag-mark:has(input:not(:checked)),
3641
+ .ck-tag-filter .tag-mark.tag-mark--off,
3642
+ .ck-metric-tag-filter .tag-mark.tag-mark--off {
3643
+ background: transparent;
3644
+ border-color: var(--ck-line);
3645
+ color: var(--ck-muted);
3646
+ }
3647
+ .ck-tag-picker label.tag-mark:has(input:not(:checked)):hover,
3648
+ .ck-tag-filter .tag-mark.tag-mark--off:hover,
3649
+ .ck-metric-tag-filter .tag-mark.tag-mark--off:hover {
3650
+ border-color: var(--ck-line-strong);
3651
+ color: color-mix(in srgb, var(--mark-color) 80%, var(--ck-text));
3652
+ }
3653
+ button.tag-mark {
3654
+ font: inherit;
3655
+ cursor: pointer;
3656
+ }
3657
+ a.tag-mark {
3658
+ text-decoration: none;
3659
+ }
3660
+
3661
+ /* Top-nav settings menu (ported from completion-kit-cloud) */
3662
+ .ck-settings-menu {
3663
+ position: relative;
3664
+ }
3665
+ .ck-settings-menu__trigger {
3666
+ cursor: pointer;
3667
+ list-style: none;
3668
+ user-select: none;
3669
+ }
3670
+ .ck-settings-menu__trigger::-webkit-details-marker {
3671
+ display: none;
3672
+ }
3673
+ .ck-settings-menu__panel {
3674
+ position: absolute;
3675
+ right: 0;
3676
+ top: calc(100% + 0.5rem);
3677
+ padding: 0.5rem;
3678
+ background: var(--ck-surface);
3679
+ border: 1px solid var(--ck-line);
3680
+ border-radius: var(--ck-radius);
3681
+ z-index: 50;
3682
+ display: flex;
3683
+ flex-direction: column;
3684
+ gap: 0.1rem;
3685
+ white-space: nowrap;
3686
+ }
3687
+ .ck-settings-menu__item {
3688
+ display: block;
3689
+ padding: 0.5rem 0.65rem;
3690
+ border-radius: calc(var(--ck-radius) - 2px);
3691
+ font-family: var(--ck-mono);
3692
+ font-size: 0.78rem;
3693
+ letter-spacing: 0.04em;
3694
+ text-transform: uppercase;
3695
+ text-decoration: none;
3696
+ color: var(--ck-text);
3697
+ transition: background 0.15s, color 0.15s;
3698
+ }
3699
+ .ck-settings-menu__item:hover {
3700
+ background: var(--ck-surface-hover);
3701
+ }
3702
+
3703
+ /* Settings page kicker — small label above the page title on settings pages */
3704
+ .ck-settings-kicker {
3705
+ display: flex;
3706
+ align-items: center;
3707
+ gap: 0.45rem;
3708
+ flex-wrap: wrap;
3709
+ font-family: var(--ck-mono);
3710
+ font-size: 11px;
3711
+ letter-spacing: 0.2em;
3712
+ text-transform: uppercase;
3713
+ color: var(--ck-dim);
3714
+ margin: 0 0 0.85rem;
3715
+ }
3716
+ .ck-settings-kicker__sep {
3717
+ opacity: 0.55;
3718
+ }
3719
+ .ck-settings-kicker__link {
3720
+ color: var(--ck-muted);
3721
+ text-decoration: none;
3722
+ transition: color 120ms ease;
3723
+ }
3724
+ .ck-settings-kicker__link:hover { color: var(--ck-text); }
3725
+
3726
+ .tag-marks-row--header {
3727
+ margin-top: -0.5rem;
3728
+ margin-bottom: 1.25rem;
3729
+ }
3730
+
3731
+
3732
+ .ck-input--error {
3733
+ border-color: var(--ck-danger);
3734
+ background: rgba(248, 113, 113, 0.05);
3735
+ }
3736
+ .ck-input--error:focus {
3737
+ box-shadow: 0 0 0 3px rgba(248, 113, 113, 0.15);
3738
+ border-color: var(--ck-danger);
3739
+ }
3740
+
3741
+ .ck-field-error {
3742
+ margin: 0.35rem 0 0;
3743
+ color: var(--ck-danger);
3744
+ font-family: var(--ck-sans);
3745
+ font-size: 0.95rem;
3746
+ }
3747
+
3748
+ /* Filter bar — its own visual moment */
3749
+ .ck-tag-filter {
3750
+ display: flex;
3751
+ flex-wrap: wrap;
3752
+ align-items: center;
3753
+ gap: 8px;
3754
+ margin: 1.1rem 0 1.6rem;
3755
+ padding: 0.65rem 0.85rem;
3756
+ background: var(--ck-surface-soft);
3757
+ border: 1px solid var(--ck-line);
3758
+ border-radius: 6px;
3759
+ }
3760
+ .ck-tag-filter__label {
3761
+ font-family: var(--ck-mono);
3762
+ font-size: 12px;
3763
+ letter-spacing: 0.12em;
3764
+ text-transform: uppercase;
3765
+ color: var(--ck-dim);
3766
+ margin-right: 0.4rem;
3767
+ }
3768
+ .ck-tag-filter__clear {
3769
+ margin-left: auto;
3770
+ font-family: var(--ck-mono);
3771
+ font-size: 11.5px;
3772
+ letter-spacing: 0.08em;
3773
+ text-transform: uppercase;
3774
+ color: var(--ck-muted);
3775
+ text-decoration: none;
3776
+ transition: color 120ms ease;
3777
+ }
3778
+ .ck-tag-filter__clear:hover { color: var(--ck-accent); }
3779
+
3780
+
@@ -5,7 +5,7 @@ module CompletionKit
5
5
  before_action :set_dataset, only: [:show, :update, :destroy]
6
6
 
7
7
  def index
8
- render json: Dataset.order(created_at: :desc)
8
+ render json: Dataset.includes(:tags).order(created_at: :desc)
9
9
  end
10
10
 
11
11
  def show
@@ -43,7 +43,7 @@ module CompletionKit
43
43
  end
44
44
 
45
45
  def dataset_params
46
- params.permit(:name, :csv_data)
46
+ params.permit(:name, :csv_data, tag_names: [])
47
47
  end
48
48
  end
49
49
  end
@@ -5,7 +5,7 @@ module CompletionKit
5
5
  before_action :set_metric_group, only: [:show, :update, :destroy]
6
6
 
7
7
  def index
8
- render json: MetricGroup.order(created_at: :desc)
8
+ render json: MetricGroup.includes(:tags).order(created_at: :desc)
9
9
  end
10
10
 
11
11
  def show
@@ -45,7 +45,7 @@ module CompletionKit
45
45
  end
46
46
 
47
47
  def metric_group_params
48
- params.permit(:name, :description, metric_ids: [])
48
+ params.permit(:name, :description, metric_ids: [], tag_names: [])
49
49
  end
50
50
  end
51
51
  end
@@ -5,7 +5,7 @@ module CompletionKit
5
5
  before_action :set_metric, only: [:show, :update, :destroy]
6
6
 
7
7
  def index
8
- render json: Metric.order(created_at: :desc)
8
+ render json: Metric.includes(:tags).order(created_at: :desc)
9
9
  end
10
10
 
11
11
  def show
@@ -43,7 +43,8 @@ module CompletionKit
43
43
  end
44
44
 
45
45
  def metric_params
46
- params.permit(:name, :instruction, rubric_bands: [:stars, :description])
46
+ params.permit(:name, :instruction,
47
+ rubric_bands: [:stars, :description], tag_names: [])
47
48
  end
48
49
  end
49
50
  end