chat_manager 1.1.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 8ddf6c7e7730f55843f51ac1ea037abbd9e071a16a06d1b40896ebee66dc9bd7
4
- data.tar.gz: 3a0769394095c3b8b5bfcae45cfd8a28c41833adefed7a5b8c16eb575da1f330
3
+ metadata.gz: 700671f665ef55917beb61c67fb22c09b9532d61047ca0749833865548a1da0c
4
+ data.tar.gz: 18f1238d5225620129679a8457577f834da9412697d7451e3c61f0ffda50b27d
5
5
  SHA512:
6
- metadata.gz: b8b1758695c42ae4ca116c3f5d771c4523d2294298826a0e872e11121b3386703745a6fcc0c2979919238dd1fbf05182562b114e2fff140dfd191fe6ceb0963a
7
- data.tar.gz: 75f3f05bb3e6cbc7bfadb25579c4a5f7cb438fbf26ad02e7b7ff2cb3dbb863e66a6d0f04d4dc7b717cabbdf418ee525c5f58d21a55e3a325d917195515cd52dd
6
+ metadata.gz: de96e54320b00a6d9347fe95a3d33c2fb8bebe50e66b0caace50902746467cf4004e11b1e76a935b221e2d5ccfdd536f73726a8ed8278509be175b5051c73d02
7
+ data.tar.gz: df7ef5b1082544b71b5232954a1ed5e98237d7517ac994542156612cfe8aaf04fc4940f9912d7043eb75af8aeb77d73f08f6fcf9e77c5987d72eedc3385ad10d
data/CHANGELOG.md CHANGED
@@ -5,6 +5,27 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
+ ## [1.2.0] - 2026-06-04
9
+
10
+ ### Added
11
+
12
+ - Gmail-style multi-select on the chat list — checkbox per card, a kebab menu (delete selected, export selected) on the list toolbar, and a "select all visible" affordance. Selection state lives in a Stimulus controller; the host still owns the destroy / export routes.
13
+
14
+ ### Changed
15
+
16
+ - Chat-list toolbar is now sticky and visually covers the host sidebar's top padding so chats scroll under it cleanly.
17
+
18
+ ### Tested
19
+
20
+ - `ChatManager::CsvDownloadable#generate_csv_for_chats` role routing + interleaving of user / assistant rows.
21
+ - `ChatManager::TitleGeneratable` short-circuit when `title` is already set, 255-char truncation, and rescue behavior on `StandardError`.
22
+
23
+ ## [1.1.1] - 2026-05-11
24
+
25
+ ### Fixed
26
+
27
+ - `ChatManager::CsvDownloadable` referenced the old `prompt_manager_prompt_execution` association name, raising `ActiveRecord::AssociationNotFoundError` when host apps had migrated to `prompt_navigator`. The concern (and README/CLAUDE.md docs) now use `prompt_navigator_prompt_execution`.
28
+
8
29
  ## [1.1.0] - 2026-04-22
9
30
 
10
31
  ### Added
data/README.md CHANGED
@@ -110,9 +110,9 @@ CSV output includes the following columns: `Chat Title`, `Role`, `Message Conten
110
110
  **Prerequisites:** The host application must provide:
111
111
 
112
112
  - `current_user` method in the controller (returning an object with a `chats` association)
113
- - `chats` association that supports `.includes(messages: :prompt_manager_prompt_execution)`
113
+ - `chats` association that supports `.includes(messages: :prompt_navigator_prompt_execution)`
114
114
  - `ordered_messages` method on the Chat model
115
- - Each message must have a `role` attribute and a `prompt_manager_prompt_execution` association with `prompt` and `response` attributes
115
+ - Each message must have a `role` attribute and a `prompt_navigator_prompt_execution` association with `prompt` and `response` attributes
116
116
 
117
117
  ### TitleGeneratable (Model Concern)
118
118
 
@@ -2,40 +2,39 @@
2
2
  position: relative;
3
3
  display: flex;
4
4
  flex-direction: column;
5
- gap: 8px;
6
- padding-left: 32px; /* space for arrows */
5
+ gap: 2px;
7
6
  }
8
7
 
9
8
  .chat-card {
10
- background: #fff;
11
- border: 1px solid #ddd;
12
- border-radius: 8px;
13
- padding: 8px 10px;
14
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.06);
9
+ background: transparent;
10
+ border: none;
11
+ border-radius: 6px;
12
+ padding: 4px 6px;
15
13
  position: relative;
16
- z-index: 1;
17
- transition:
18
- box-shadow 0.15s ease,
19
- transform 0.15s ease;
14
+ transition: background-color 0.15s ease;
20
15
 
21
16
  &.is-active {
22
- border-color: #007bff;
23
- box-shadow: 0 0 0 2px rgba(0, 123, 255, 0.25);
17
+ background-color: rgba(0, 123, 255, 0.10);
24
18
  }
25
19
 
26
20
  &:hover {
27
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.12);
28
- transform: translateY(-2px);
21
+ background-color: rgba(0, 0, 0, 0.05);
22
+ }
23
+
24
+ &.is-active:hover {
25
+ background-color: rgba(0, 123, 255, 0.14);
26
+ }
27
+
28
+ &.is-selected {
29
+ background-color: #eef5ff;
29
30
  }
30
31
  }
31
32
 
32
33
  .chat-card-link {
33
- display: grid;
34
- grid-template-columns: auto 1fr auto;
35
- grid-gap: 8px;
34
+ display: block;
36
35
  text-decoration: none;
37
- color: #333;
38
- align-items: center;
36
+ color: #222;
37
+ min-width: 0;
39
38
  }
40
39
 
41
40
  .chat-card-number {
@@ -45,18 +44,20 @@
45
44
  }
46
45
 
47
46
  .chat-card-prompt {
48
- font-size: 12px;
49
- line-height: 1.3;
47
+ font-size: 13px;
48
+ line-height: 1.35;
50
49
  overflow: hidden;
51
50
  display: -webkit-box;
52
- -webkit-line-clamp: 2;
51
+ -webkit-line-clamp: 1;
53
52
  -webkit-box-orient: vertical;
53
+ white-space: nowrap;
54
+ text-overflow: ellipsis;
54
55
  }
55
56
 
56
57
  .chat-card-row {
57
58
  display: flex;
58
59
  align-items: center;
59
- gap: 4px;
60
+ gap: 6px;
60
61
  }
61
62
 
62
63
  .chat-card-row .chat-card-link {
@@ -64,90 +65,214 @@
64
65
  min-width: 0;
65
66
  }
66
67
 
67
- .chat-card-download {
68
+ .chat-card-menu-wrap {
69
+ position: relative;
70
+ flex-shrink: 0;
71
+ }
72
+
73
+ .chat-card-menu-btn {
68
74
  display: flex;
69
75
  align-items: center;
70
76
  justify-content: center;
71
- width: 24px;
72
- height: 24px;
77
+ width: 22px;
78
+ height: 22px;
79
+ padding: 0;
80
+ border: none;
81
+ background: transparent;
73
82
  border-radius: 4px;
74
- color: #888;
75
- text-decoration: none;
76
- flex-shrink: 0;
77
- transition: color 0.15s ease, background-color 0.15s ease;
83
+ color: #777;
84
+ cursor: pointer;
85
+ opacity: 0;
86
+ transition: opacity 0.15s ease, color 0.15s ease, background-color 0.15s ease;
87
+
88
+ i {
89
+ font-size: 14px;
90
+ }
78
91
 
79
92
  &:hover {
80
- color: #007bff;
81
- background-color: rgba(0, 123, 255, 0.1);
93
+ color: #222;
94
+ background-color: rgba(0, 0, 0, 0.08);
82
95
  }
96
+ }
97
+
98
+ .chat-card:hover .chat-card-menu-btn,
99
+ .chat-card-menu-wrap:has(.chat-card-menu.open) .chat-card-menu-btn {
100
+ opacity: 1;
101
+ }
102
+
103
+ .chat-card-menu {
104
+ position: absolute;
105
+ top: 100%;
106
+ right: 0;
107
+ margin-top: 4px;
108
+ min-width: 160px;
109
+ background: #fff;
110
+ border: 1px solid #e5e7eb;
111
+ border-radius: 6px;
112
+ box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
113
+ padding: 4px;
114
+ z-index: 20;
115
+ display: none;
116
+ }
117
+
118
+ .chat-card-menu.open {
119
+ display: flex;
120
+ flex-direction: column;
121
+ }
122
+
123
+ .chat-card-menu-item {
124
+ display: flex;
125
+ align-items: center;
126
+ gap: 8px;
127
+ padding: 6px 8px;
128
+ font-size: 12px;
129
+ color: #333;
130
+ text-decoration: none;
131
+ border-radius: 4px;
132
+ border: none;
133
+ background: transparent;
134
+ cursor: pointer;
135
+ text-align: left;
136
+ width: 100%;
137
+ box-sizing: border-box;
83
138
 
84
139
  i {
85
140
  font-size: 12px;
86
141
  }
142
+
143
+ &:hover {
144
+ background-color: #f3f4f6;
145
+ }
146
+ }
147
+
148
+ .chat-card-menu-item-danger {
149
+ color: #dc2626;
150
+
151
+ &:hover {
152
+ background-color: rgba(220, 38, 38, 0.08);
153
+ }
87
154
  }
88
155
 
89
- .chat-card-delete-form {
156
+ .chat-card-menu-form {
90
157
  margin: 0;
158
+ width: 100%;
159
+ }
160
+
161
+ .chat-list-toolbar {
162
+ display: flex;
163
+ align-items: center;
164
+ gap: 8px;
165
+ min-height: 28px;
166
+ /* Pin the heading + bulk-action row to the top of the scrollable
167
+ sidebar so it stays accessible when the chat list is long. The
168
+ opaque background masks cards scrolling underneath; the explicit
169
+ z-index lifts the toolbar above sibling cards in stacking. The
170
+ negative top/side margins extend the toolbar across the host
171
+ sidebar's 8px padding so scrolling cards can't peek through the
172
+ gap above it; equal padding keeps the toolbar's content in its
173
+ natural place. */
174
+ position: sticky;
175
+ top: 0;
176
+ background-color: #ffffff;
177
+ z-index: 2;
178
+ margin: -8px -8px 6px;
179
+ padding: 8px 8px 0;
180
+
181
+ h2 {
182
+ margin: 0;
183
+ font-size: 14px;
184
+ line-height: 1.2;
185
+ }
186
+ }
187
+
188
+ .chat-list-select-all {
189
+ width: 16px;
190
+ height: 16px;
191
+ margin: 0;
192
+ cursor: pointer;
193
+ accent-color: #007bff;
194
+ flex-shrink: 0;
195
+ }
196
+
197
+ .chat-list-heading {
198
+ flex: 1;
199
+ min-width: 0;
200
+ }
201
+
202
+ .chat-list-bar {
203
+ flex: 1;
204
+ display: flex;
205
+ align-items: center;
206
+ justify-content: space-between;
207
+ gap: 8px;
208
+ min-width: 0;
209
+ }
210
+
211
+ .chat-list-toolbar .hidden {
212
+ display: none;
213
+ }
214
+
215
+ .chat-list-bar-count {
216
+ font-size: 12px;
217
+ color: #555;
218
+ white-space: nowrap;
219
+ }
220
+
221
+ .chat-list-bar-actions {
91
222
  display: flex;
92
223
  align-items: center;
224
+ gap: 4px;
93
225
  }
94
226
 
95
- .chat-card-delete {
227
+ .chat-list-bar-download,
228
+ .chat-list-bar-delete {
96
229
  display: flex;
97
230
  align-items: center;
98
231
  justify-content: center;
99
- width: 24px;
100
- height: 24px;
232
+ width: 28px;
233
+ height: 28px;
101
234
  padding: 0;
102
235
  border: none;
103
236
  background: transparent;
104
237
  border-radius: 4px;
105
- color: #888;
106
238
  cursor: pointer;
107
- flex-shrink: 0;
239
+ color: #555;
108
240
  transition: color 0.15s ease, background-color 0.15s ease;
109
241
 
110
- &:hover {
111
- color: #dc2626;
112
- background-color: rgba(220, 38, 38, 0.1);
113
- }
114
-
115
242
  i {
116
- font-size: 12px;
243
+ font-size: 14px;
117
244
  }
118
245
  }
119
246
 
120
- .chat-download-all {
121
- margin-top: 12px;
122
- padding-left: 32px;
247
+ .chat-list-bar-download:hover {
248
+ color: #007bff;
249
+ background-color: rgba(0, 123, 255, 0.1);
123
250
  }
124
251
 
125
- .chat-download-all-link {
126
- display: inline-flex;
127
- align-items: center;
128
- gap: 6px;
129
- font-size: 12px;
130
- color: #555;
131
- text-decoration: none;
132
- padding: 6px 10px;
133
- border: 1px solid #ddd;
134
- border-radius: 6px;
135
- transition: color 0.15s ease, border-color 0.15s ease, background-color 0.15s ease;
252
+ .chat-list-bar-delete:hover {
253
+ color: #dc2626;
254
+ background-color: rgba(220, 38, 38, 0.1);
255
+ }
136
256
 
137
- &:hover {
138
- color: #007bff;
139
- border-color: #007bff;
140
- background-color: rgba(0, 123, 255, 0.05);
141
- }
257
+ .chat-card-checkbox {
258
+ width: 14px;
259
+ height: 14px;
260
+ margin: 0;
261
+ cursor: pointer;
262
+ accent-color: #007bff;
263
+ flex-shrink: 0;
264
+ opacity: 0;
265
+ transition: opacity 0.15s ease;
266
+ }
142
267
 
143
- i {
144
- font-size: 12px;
145
- }
268
+ .chat-card:hover .chat-card-checkbox,
269
+ .chat-card-checkbox:checked {
270
+ opacity: 1;
146
271
  }
147
272
 
148
273
  .chat-card-title-input {
149
- font-size: 12px;
150
- line-height: 1.3;
274
+ font-size: 13px;
275
+ line-height: 1.35;
151
276
  width: 100%;
152
277
  padding: 2px 4px;
153
278
  border: 1px solid #ccc;
@@ -4,6 +4,7 @@
4
4
  card_path = locals[:card_path]
5
5
  download_csv_path = locals[:download_csv_path]
6
6
  delete_path = locals[:delete_path]
7
+ selectable = locals[:selectable]
7
8
  %>
8
9
 
9
10
  <div class="chat-card<%= ' is-active' if is_active %>"
@@ -14,24 +15,45 @@
14
15
  data-chat-title-edit-full-title-value="<%= ann.title.to_s %>"
15
16
  data-chat-title-edit-update-url-value="<%= main_app.update_title_chat_path(ann.uuid) %>">
16
17
  <div class="chat-card-row">
18
+ <% if selectable %>
19
+ <input type="checkbox"
20
+ class="chat-card-checkbox"
21
+ data-chat-target="checkbox"
22
+ data-uuid="<%= ann.uuid %>"
23
+ data-action="click->chat#toggle" />
24
+ <% end %>
17
25
  <%= link_to card_path.call(ann.uuid), class: "chat-card-link", data: { "chat-title-edit-target": "link" } do %>
18
26
  <div class="chat-card-prompt" data-chat-title-edit-target="display"><%= truncate(ann.title.to_s, length: 30) %></div>
19
27
  <% end %>
20
- <% if download_csv_path %>
21
- <%= link_to download_csv_path.call(ann.uuid), class: "chat-card-download", title: "Download CSV", data: { turbo: false } do %>
22
- <i class="bi bi-download"></i>
23
- <% end %>
24
- <% end %>
25
- <% if delete_path %>
26
- <%= button_to delete_path.call(ann.uuid),
27
- method: :delete,
28
- class: "chat-card-delete",
29
- title: "Delete chat",
30
- form: { class: "chat-card-delete-form" },
31
- data: { turbo_confirm: "Delete this chat? This cannot be undone." } do %>
32
- <i class="bi bi-trash"></i>
33
- <% end %>
34
- <% end %>
28
+ <div class="chat-card-menu-wrap" data-controller="chat-menu">
29
+ <button type="button"
30
+ class="chat-card-menu-btn"
31
+ title="More actions"
32
+ data-action="click->chat-menu#toggle">
33
+ <i class="bi bi-three-dots-vertical"></i>
34
+ </button>
35
+ <div class="chat-card-menu" data-chat-menu-target="menu">
36
+ <button type="button"
37
+ class="chat-card-menu-item"
38
+ data-action="click->chat-menu#close click->chat-title-edit#start">
39
+ <i class="bi bi-pencil"></i> Rename
40
+ </button>
41
+ <% if delete_path %>
42
+ <%= button_to delete_path.call(ann.uuid),
43
+ method: :delete,
44
+ class: "chat-card-menu-item chat-card-menu-item-danger",
45
+ form: { class: "chat-card-menu-form" },
46
+ data: { turbo_confirm: "Delete this chat? This cannot be undone." } do %>
47
+ <i class="bi bi-trash"></i> Delete
48
+ <% end %>
49
+ <% end %>
50
+ <% if download_csv_path %>
51
+ <%= link_to download_csv_path.call(ann.uuid), class: "chat-card-menu-item", data: { turbo: false } do %>
52
+ <i class="bi bi-download"></i> Download
53
+ <% end %>
54
+ <% end %>
55
+ </div>
56
+ </div>
35
57
  </div>
36
58
  <input type="text" class="chat-card-title-input"
37
59
  data-chat-title-edit-target="input"
@@ -2,25 +2,57 @@
2
2
  active_uuid = locals[:active_uuid]
3
3
  card_path = locals[:card_path]
4
4
  download_csv_path = locals[:download_csv_path]
5
- download_all_csv_path = locals[:download_all_csv_path]
6
5
  delete_path = locals[:delete_path]
7
- titled_chats = @chats.select { |c| c.title.present? }
6
+ batch_delete_path = locals[:batch_delete_path]
7
+ batch_download_csv_path = locals[:batch_download_csv_path]
8
+ titled_chats = @chats.select { |c| c.title.present? }.reverse
9
+ selectable = batch_delete_path.present? || batch_download_csv_path.present?
8
10
  %>
9
11
 
10
- <h2>chat</h2>
11
12
  <% if titled_chats.present? %>
12
- <div class="chat-stack" data-controller="chat">
13
+ <div class="chat-stack"
14
+ data-controller="chat"
15
+ data-chat-batch-delete-path-value="<%= batch_delete_path %>"
16
+ data-chat-batch-download-csv-path-value="<%= batch_download_csv_path %>">
17
+ <div class="chat-list-toolbar">
18
+ <div class="chat-list-heading" data-chat-target="heading">
19
+ <h2>Chats</h2>
20
+ </div>
21
+ <div class="chat-list-bar hidden" data-chat-target="bar">
22
+ <span class="chat-list-bar-count">
23
+ <span data-chat-target="selectedCount">0</span> selected
24
+ </span>
25
+ <div class="chat-list-bar-actions">
26
+ <% if batch_download_csv_path %>
27
+ <button type="button"
28
+ class="chat-list-bar-download"
29
+ title="Download selected as CSV"
30
+ data-action="click->chat#bulkDownload">
31
+ <i class="bi bi-download"></i>
32
+ </button>
33
+ <% end %>
34
+ <% if batch_delete_path %>
35
+ <button type="button"
36
+ class="chat-list-bar-delete"
37
+ title="Delete selected"
38
+ data-action="click->chat#bulkDelete">
39
+ <i class="bi bi-trash"></i>
40
+ </button>
41
+ <% end %>
42
+ </div>
43
+ </div>
44
+ <% if selectable %>
45
+ <input type="checkbox"
46
+ class="chat-list-select-all"
47
+ data-chat-target="selectAll"
48
+ data-action="change->chat#toggleAll"
49
+ title="Select all" />
50
+ <% end %>
51
+ </div>
13
52
  <% titled_chats.each_with_index do |ann, idx| %>
14
- <%= render 'chat_manager/chat_card', locals: { ann: ann, next_ann: titled_chats[idx + 1], is_active: ann.id == active_uuid, card_path: card_path, download_csv_path: download_csv_path, delete_path: delete_path } %>
53
+ <%= render 'chat_manager/chat_card', locals: { ann: ann, next_ann: titled_chats[idx + 1], is_active: ann.uuid == active_uuid, card_path: card_path, download_csv_path: download_csv_path, delete_path: delete_path, selectable: selectable } %>
15
54
  <% end %>
16
55
  </div>
17
- <% if download_all_csv_path %>
18
- <div class="chat-download-all">
19
- <%= link_to download_all_csv_path, class: "chat-download-all-link", data: { turbo: false } do %>
20
- <i class="bi bi-download"></i> Download All Chats CSV
21
- <% end %>
22
- </div>
23
- <% end %>
24
56
  <% else %>
25
57
  <p class="chat-empty">No chat yet</p>
26
58
  <% end %>
@@ -9,7 +9,7 @@ module ChatManager
9
9
  CSV_HEADERS = [ "Chat Title", "Role", "Message Content", "Sent At", "Model" ].freeze
10
10
 
11
11
  def download_csv
12
- chat = current_user.chats.includes(messages: :prompt_manager_prompt_execution).find_by!(uuid: params[:id])
12
+ chat = current_user.chats.includes(messages: :prompt_navigator_prompt_execution).find_by!(uuid: params[:id])
13
13
 
14
14
  csv_data = generate_csv_for_chats([ chat ])
15
15
 
@@ -17,12 +17,13 @@ module ChatManager
17
17
  send_data csv_data, filename: filename, type: "text/csv"
18
18
  end
19
19
 
20
- def download_all_csv
21
- chats = current_user.chats.includes(messages: :prompt_manager_prompt_execution)
20
+ def download_selected_csv
21
+ uuids = Array(params[:uuids]).reject(&:blank?)
22
+ chats = current_user.chats.where(uuid: uuids).includes(messages: :prompt_navigator_prompt_execution)
22
23
 
23
24
  csv_data = generate_csv_for_chats(chats)
24
25
 
25
- send_data csv_data, filename: "all_chats_#{Date.today}.csv", type: "text/csv"
26
+ send_data csv_data, filename: "chats_#{Date.today}.csv", type: "text/csv"
26
27
  end
27
28
 
28
29
  private
@@ -32,7 +33,7 @@ module ChatManager
32
33
  csv << CSV_HEADERS
33
34
  chats.each do |chat|
34
35
  chat.ordered_messages.each do |msg|
35
- pe = msg.prompt_manager_prompt_execution
36
+ pe = msg.prompt_navigator_prompt_execution
36
37
  content = msg.role == "user" ? pe&.prompt : pe&.response
37
38
  csv << [ chat.title, msg.role, content, msg.created_at, chat.model ]
38
39
  end
@@ -1,7 +1,19 @@
1
1
  module ChatManager
2
2
  module Helpers
3
- def chat_list(card_path, active_uuid: nil, download_csv_path: nil, download_all_csv_path: nil, delete_path: nil)
4
- render "chat_manager/chat_list", locals: { card_path: card_path, active_uuid: active_uuid, download_csv_path: download_csv_path, download_all_csv_path: download_all_csv_path, delete_path: delete_path }
3
+ def chat_list(card_path,
4
+ active_uuid: nil,
5
+ download_csv_path: nil,
6
+ delete_path: nil,
7
+ batch_delete_path: nil,
8
+ batch_download_csv_path: nil)
9
+ render "chat_manager/chat_list", locals: {
10
+ card_path: card_path,
11
+ active_uuid: active_uuid,
12
+ download_csv_path: download_csv_path,
13
+ delete_path: delete_path,
14
+ batch_delete_path: batch_delete_path,
15
+ batch_download_csv_path: batch_download_csv_path
16
+ }
5
17
  end
6
18
  end
7
19
  end
@@ -1,3 +1,3 @@
1
1
  module ChatManager
2
- VERSION = "1.1.0"
2
+ VERSION = "1.2.0"
3
3
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: chat_manager
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.1.0
4
+ version: 1.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - dhq_boiler