feat: swipe picker expand/collapse, copy button, and left-align text (#5380) * Initial plan * feat: add expand/collapse, copy button, and left-align text in swipe picker Addresses user feedback for swipe picker (added in 1.17.0): 1. Left-align swipe text (was inheriting centered alignment) 2. Add expand/collapse toggle using hidden checkbox + CSS :has 3. Add copy-to-clipboard button using shared copyText utility Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/90892f1c-3c75-404b-a93b-2abe672cc0e3 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * revert unintended package-lock.json changes from npm install Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/90892f1c-3c75-404b-a93b-2abe672cc0e3 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: address review feedback - hide checkbox, use chevron icons, align buttons with fa-fw - Fix checkbox visibility by using !important to override input[type=checkbox] grid display - Change expand icon to fa-chevron-down and collapse to fa-chevron-up - Add fa-fw class to all action buttons (copy, expand/collapse, branch, delete) for even sizing - Add align-items: baseline to the actions container Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/c8ca349f-3579-442a-baa1-fc138ba1d7a6 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * revert unintended package-lock.json changes Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/c8ca349f-3579-442a-baa1-fc138ba1d7a6 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * fix: update class names for chat action elements in swipe picker * fix: prevent chevron layout shift and swap button order - Use single label element as icon (fa-chevron-down) instead of two child <i> elements toggling visibility, preventing layout shift - Override ::before content to chevron-up when expanded via CSS - Swap chevron and copy button order (chevron first, then copy) Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/284283b1-6c7f-4371-bc4a-94d841513879 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * style: add comment for FA unicode value in CSS Agent-Logs-Url: https://github.com/SillyTavern/SillyTavern/sessions/284283b1-6c7f-4371-bc4a-94d841513879 Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com> * Rotate the chevron instead of changing content * Make the ui full height --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: Cohee1207 <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -6733,7 +6733,7 @@ | |||
| 6733 | <small class="chat_file_size select_chat_block_filename_item"></small> | 6733 | <small class="chat_file_size select_chat_block_filename_item"></small> |
| 6734 | <small class="chat_messages_num select_chat_block_filename_item"></small> | 6734 | <small class="chat_messages_num select_chat_block_filename_item"></small> |
| 6735 | </div> | 6735 | </div> |
| 6736 | <div class="flex-container gap10px"> | 6736 | <div class="select_chat_actions flex-container gap10px"> |
| 6737 | <div title="Export JSONL chat file" data-format="jsonl" class="exportRawChatButton opacity50p hoverglow fa-solid fa-file-export" data-i18n="[title]Export JSONL chat file"></div> | 6737 | <div title="Export JSONL chat file" data-format="jsonl" class="exportRawChatButton opacity50p hoverglow fa-solid fa-file-export" data-i18n="[title]Export JSONL chat file"></div> |
| 6738 | <div title="Download chat as plain text document" data-format="txt" class="exportChatButton opacity50p hoverglow fa-solid fa-file-lines" data-i18n="[title]Download chat as plain text document"></div> | 6738 | <div title="Download chat as plain text document" data-format="txt" class="exportChatButton opacity50p hoverglow fa-solid fa-file-lines" data-i18n="[title]Download chat as plain text document"></div> |
| 6739 | <div title="Delete chat file" file_name="" class="PastChat_cross opacity50p hoverglow fa-solid fa-skull" data-i18n="[title]Delete chat file"></div> | 6739 | <div title="Delete chat file" file_name="" class="PastChat_cross opacity50p hoverglow fa-solid fa-skull" data-i18n="[title]Delete chat file"></div> |
| @@ -4,7 +4,7 @@ import { t } from './i18n.js'; | |||
| 4 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; | 4 | import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js'; |
| 5 | import { power_user } from './power-user.js'; | 5 | import { power_user } from './power-user.js'; |
| 6 | import { getTokenCountAsync } from './tokenizers.js'; | 6 | import { getTokenCountAsync } from './tokenizers.js'; |
| 7 | import { clamp, timestampToMoment } from './utils.js'; | 7 | import { clamp, copyText, timestampToMoment } from './utils.js'; |
| 8 | import { chat, deleteSwipe, ensureSwipes, isMessageSwipeable, isSwipingAllowed, swipe, syncMesToSwipe } from '/script.js'; | 8 | import { chat, deleteSwipe, ensureSwipes, isMessageSwipeable, isSwipingAllowed, swipe, syncMesToSwipe } from '/script.js'; |
| 9 | 9 | ||
| 10 | /** | 10 | /** |
| @@ -132,6 +132,7 @@ async function openSwipePicker(messageId) { | |||
| 132 | const template = $('#past_chat_template .select_chat_block_wrapper').clone(); | 132 | const template = $('#past_chat_template .select_chat_block_wrapper').clone(); |
| 133 | const block = template.find('.select_chat_block'); | 133 | const block = template.find('.select_chat_block'); |
| 134 | block.removeClass('select_chat_block').addClass('swipe_picker_block'); | 134 | block.removeClass('select_chat_block').addClass('swipe_picker_block'); |
| 135 | block.find('.select_chat_actions').removeClass('gap10px'); | ||
| 135 | const branchButton = template.find('.exportRawChatButton'); | 136 | const branchButton = template.find('.exportRawChatButton'); |
| 136 | const deleteButton = template.find('.PastChat_cross'); | 137 | const deleteButton = template.find('.PastChat_cross'); |
| 137 | const swipeInfo = Array.isArray(message.swipe_info) ? message.swipe_info[index] : null; | 138 | const swipeInfo = Array.isArray(message.swipe_info) ? message.swipe_info[index] : null; |
| @@ -162,7 +163,7 @@ async function openSwipePicker(messageId) { | |||
| 162 | 'data-i18n': '[title]Create Branch', | 163 | 'data-i18n': '[title]Create Branch', |
| 163 | }) | 164 | }) |
| 164 | .removeClass('exportRawChatButton fa-solid fa-file-export') | 165 | .removeClass('exportRawChatButton fa-solid fa-file-export') |
| 165 | .addClass('swipe_picker_branch mes_button fa-regular fa-code-branch') | 166 | .addClass('swipe_picker_branch mes_button fa-fw fa-regular fa-code-branch') |
| 166 | .on('click', async (event) => { | 167 | .on('click', async (event) => { |
| 167 | event.preventDefault(); | 168 | event.preventDefault(); |
| 168 | event.stopPropagation(); | 169 | event.stopPropagation(); |
| @@ -174,7 +175,7 @@ async function openSwipePicker(messageId) { | |||
| 174 | .removeAttr('file_name') | 175 | .removeAttr('file_name') |
| 175 | .attr('aria-disabled', String(!canDeleteSwipe)) | 176 | .attr('aria-disabled', String(!canDeleteSwipe)) |
| 176 | .removeClass('fa-skull') | 177 | .removeClass('fa-skull') |
| 177 | .addClass('swipe_picker_delete fa-trash-can') | 178 | .addClass('swipe_picker_delete fa-fw fa-trash-can') |
| 178 | .toggleClass('hoverglow', canDeleteSwipe) | 179 | .toggleClass('hoverglow', canDeleteSwipe) |
| 179 | .toggleClass('disabled', !canDeleteSwipe) | 180 | .toggleClass('disabled', !canDeleteSwipe) |
| 180 | .each(function () { | 181 | .each(function () { |
| @@ -229,11 +230,42 @@ async function openSwipePicker(messageId) { | |||
| 229 | 230 | ||
| 230 | await renderSwipeList(); | 231 | await renderSwipeList(); |
| 231 | }); | 232 | }); |
| 233 | |||
| 234 | // Add expand/collapse toggle | ||
| 235 | const expandCheckboxId = `swipe_picker_expand_${messageId}_${index}`; | ||
| 236 | const expandCheckbox = document.createElement('input'); | ||
| 237 | expandCheckbox.type = 'checkbox'; | ||
| 238 | expandCheckbox.id = expandCheckboxId; | ||
| 239 | expandCheckbox.classList.add('swipe_picker_expand_toggle'); | ||
| 240 | block[0].prepend(expandCheckbox); | ||
| 241 | |||
| 242 | const expandLabel = document.createElement('label'); | ||
| 243 | expandLabel.htmlFor = expandCheckboxId; | ||
| 244 | expandLabel.classList.add('swipe_picker_expand_label', 'fa-solid', 'fa-fw', 'fa-chevron-down'); | ||
| 245 | expandLabel.title = t`Expand/Collapse`; | ||
| 246 | expandLabel.setAttribute('data-i18n', '[title]Expand/Collapse'); | ||
| 247 | expandLabel.addEventListener('click', (event) => event.stopPropagation()); | ||
| 248 | |||
| 249 | // Add copy button | ||
| 250 | const copyButton = document.createElement('div'); | ||
| 251 | copyButton.classList.add('swipe_picker_copy', 'fa-solid', 'fa-fw', 'fa-copy'); | ||
| 252 | copyButton.title = t`Copy`; | ||
| 253 | copyButton.setAttribute('data-i18n', '[title]Copy'); | ||
| 254 | copyButton.addEventListener('click', async (event) => { | ||
| 255 | event.preventDefault(); | ||
| 256 | event.stopPropagation(); | ||
| 257 | await copyText(swipeText); | ||
| 258 | toastr.info(t`Copied!`, '', { timeOut: 2000 }); | ||
| 259 | }); | ||
| 260 | |||
| 261 | // Insert new buttons before the branch button | ||
| 262 | branchButton.before(expandLabel, copyButton); | ||
| 263 | |||
| 232 | template.find('.select_chat_block_filename').text(`#${index + 1}${index === Number(message.swipe_id ?? 0) ? ` ${t`[Current]`}` : ''}`); | 264 | template.find('.select_chat_block_filename').text(`#${index + 1}${index === Number(message.swipe_id ?? 0) ? ` ${t`[Current]`}` : ''}`); |
| 233 | template.find('.chat_messages_date').text(sendDate); | 265 | template.find('.chat_messages_date').text(sendDate); |
| 234 | template.find('.chat_file_size').text(swipeDetails.length ? `(${swipeDetails[0]}${swipeDetails.length > 1 ? ',' : ')'}` : ''); | 266 | template.find('.chat_file_size').text(swipeDetails.length ? `(${swipeDetails[0]}${swipeDetails.length > 1 ? ',' : ')'}` : ''); |
| 235 | template.find('.chat_messages_num').text(swipeDetails.length > 1 ? `${swipeDetails.slice(1).join(', ')})` : ''); | 267 | template.find('.chat_messages_num').text(swipeDetails.length > 1 ? `${swipeDetails.slice(1).join(', ')})` : ''); |
| 236 | template.find('.select_chat_block_mes').text(previewText || t`(empty swipe)`); | 268 | template.find('.select_chat_block_mes').text(previewText ? swipeText : t`(empty swipe)`); |
| 237 | 269 | ||
| 238 | block.on('click', () => setSelectedSwipe(index)); | 270 | block.on('click', () => setSelectedSwipe(index)); |
| 239 | block.on('dblclick', async () => { | 271 | block.on('dblclick', async () => { |
| @@ -269,6 +301,7 @@ async function openSwipePicker(messageId) { | |||
| 269 | defaultState: String(selectedSwipeId + 1), | 301 | defaultState: String(selectedSwipeId + 1), |
| 270 | tooltip: `1-${message.swipes.length}`, | 302 | tooltip: `1-${message.swipes.length}`, |
| 271 | }], | 303 | }], |
| 304 | large: true, | ||
| 272 | wider: true, | 305 | wider: true, |
| 273 | allowVerticalScrolling: true, | 306 | allowVerticalScrolling: true, |
| 274 | onOpen: function () { | 307 | onOpen: function () { |
| @@ -4788,6 +4788,48 @@ h5 { | |||
| 4788 | user-select: none; | 4788 | user-select: none; |
| 4789 | } | 4789 | } |
| 4790 | 4790 | ||
| 4791 | .swipe_picker_block .select_chat_block_mes { | ||
| 4792 | text-align: left; | ||
| 4793 | } | ||
| 4794 | |||
| 4795 | .swipe_picker_block .select_chat_actions { | ||
| 4796 | align-items: baseline; | ||
| 4797 | } | ||
| 4798 | |||
| 4799 | .swipe_picker_expand_toggle { | ||
| 4800 | display: none !important; | ||
| 4801 | } | ||
| 4802 | |||
| 4803 | .swipe_picker_block:has(.swipe_picker_expand_toggle:checked) .select_chat_block_mes { | ||
| 4804 | display: block; | ||
| 4805 | -webkit-line-clamp: unset; | ||
| 4806 | line-clamp: unset; | ||
| 4807 | white-space: pre-wrap; | ||
| 4808 | overflow-wrap: anywhere; | ||
| 4809 | } | ||
| 4810 | |||
| 4811 | .swipe_picker_expand_label { | ||
| 4812 | cursor: pointer; | ||
| 4813 | opacity: 0.5; | ||
| 4814 | } | ||
| 4815 | |||
| 4816 | .swipe_picker_expand_label:hover { | ||
| 4817 | opacity: 1; | ||
| 4818 | } | ||
| 4819 | |||
| 4820 | .swipe_picker_copy { | ||
| 4821 | cursor: pointer; | ||
| 4822 | opacity: 0.5; | ||
| 4823 | } | ||
| 4824 | |||
| 4825 | .swipe_picker_copy:hover { | ||
| 4826 | opacity: 1; | ||
| 4827 | } | ||
| 4828 | |||
| 4829 | .swipe_picker_block:has(.swipe_picker_expand_toggle:checked) .swipe_picker_expand_label { | ||
| 4830 | transform: rotate(180deg); | ||
| 4831 | } | ||
| 4832 | |||
| 4791 | .select_chat_block .avatar { | 4833 | .select_chat_block .avatar { |
| 4792 | grid-row: span 2; | 4834 | grid-row: span 2; |
| 4793 | } | 4835 | } |