d2b2b1b4a6
* fix: require long press to open swipe picker on phones * fix: clarify parameter description in assignLorebookToChat function * fix: update event parameter type in onSwipeCounterClick to include TouchEvent * fix: update event parameter types in onSwipeCounterClick and addLongPressEvent
445 lines
18 KiB
JavaScript
445 lines
18 KiB
JavaScript
import { branchChat } from './bookmarks.js';
|
|
import { SWIPE_DIRECTION, SWIPE_SOURCE } from './constants.js';
|
|
import { t } from './i18n.js';
|
|
import { callGenericPopup, Popup, POPUP_RESULT, POPUP_TYPE } from './popup.js';
|
|
import { power_user } from './power-user.js';
|
|
import { isMobile } from './RossAscends-mods.js';
|
|
import { getTokenCountAsync } from './tokenizers.js';
|
|
import { addLongPressEvent, clamp, copyText, timestampToMoment } from './utils.js';
|
|
import { chat, deleteSwipe, ensureSwipes, isMessageSwipeable, isSwipingAllowed, swipe, syncMesToSwipe } from '/script.js';
|
|
|
|
/**
|
|
* Returns whether a swipe picker can be opened for the message.
|
|
* Unlike message swiping, this supports historical AI messages for inspection and branching.
|
|
* @param {number} messageId
|
|
* @returns {boolean}
|
|
*/
|
|
export function canOpenSwipePickerForMessage(messageId) {
|
|
const message = chat[messageId];
|
|
|
|
if (!message) {
|
|
return false;
|
|
}
|
|
|
|
if (ensureSwipes(message)) {
|
|
syncMesToSwipe(messageId);
|
|
}
|
|
|
|
return Boolean(
|
|
message?.swipes?.length > 1 &&
|
|
!message?.is_user &&
|
|
!(message?.extra?.isSmallSys) &&
|
|
!(message?.extra?.swipeable === false),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Returns whether the picker can actively jump to a different swipe.
|
|
* Historical AI messages can open the picker, but only the currently swipeable message may jump.
|
|
* @param {number} messageId
|
|
* @returns {boolean}
|
|
*/
|
|
export function canJumpToSwipeForMessage(messageId) {
|
|
const message = chat[messageId];
|
|
return canOpenSwipePickerForMessage(messageId) && isSwipingAllowed() && isMessageSwipeable(messageId, message);
|
|
}
|
|
|
|
/**
|
|
* Opens a popup for viewing or jumping to a specific swipe on a message.
|
|
* @param {number} messageId
|
|
* @returns {Promise<void>}
|
|
*/
|
|
async function openSwipePicker(messageId) {
|
|
const message = chat[messageId];
|
|
|
|
if (!canOpenSwipePickerForMessage(messageId)) {
|
|
toastr.info(t`This message has no alternate swipes yet.`, t`Jump to Swipe`);
|
|
return;
|
|
}
|
|
|
|
const canJumpToSwipe = canJumpToSwipeForMessage(messageId);
|
|
let selectedSwipeId = clamp(Number(message.swipe_id ?? 0), 0, message.swipes.length - 1);
|
|
const swipeIdInputId = `swipe_picker_id_${messageId}`;
|
|
const wrapper = document.createElement('div');
|
|
wrapper.classList.add('flex-container', 'flexFlowColumn', 'flexNoGap', 'wide100p', 'flex1', 'overflowHidden');
|
|
|
|
const header = document.createElement('div');
|
|
header.classList.add('swipe_picker_header', 'flex-container', 'alignItemsCenter', 'justifySpaceBetween', 'gap10px');
|
|
|
|
const description = document.createElement('h3');
|
|
description.classList.add('margin0', 'justifyLeft');
|
|
description.textContent = t`Swipe Selection`;
|
|
header.appendChild(description);
|
|
wrapper.appendChild(header);
|
|
|
|
const listContainer = document.createElement('div');
|
|
listContainer.classList.add('swipe_picker_div', 'flex1', 'marginTop10');
|
|
wrapper.appendChild(listContainer);
|
|
|
|
/** @type {Popup} */
|
|
let popup;
|
|
/** @type {HTMLInputElement} */
|
|
let swipeIdInput;
|
|
/** @type {number|null} */
|
|
let branchActionSwipeId = null;
|
|
|
|
function syncSwipeIdInput() {
|
|
if (swipeIdInput) {
|
|
swipeIdInput.value = String(selectedSwipeId + 1);
|
|
}
|
|
}
|
|
|
|
function setSelectedSwipe(nextSwipeId) {
|
|
selectedSwipeId = clamp(Number(nextSwipeId), 0, message.swipes.length - 1);
|
|
listContainer.querySelectorAll('.swipe_picker_block').forEach((element) => {
|
|
const isSelected = Number(element.getAttribute('data-swipe-id')) === selectedSwipeId;
|
|
if (isSelected) {
|
|
element.setAttribute('highlight', 'true');
|
|
} else {
|
|
element.removeAttribute('highlight');
|
|
}
|
|
});
|
|
syncSwipeIdInput();
|
|
}
|
|
|
|
function scrollToSelectedSwipe() {
|
|
const swipeBlock = listContainer.querySelector(`.swipe_picker_block[data-swipe-id="${selectedSwipeId}"]`);
|
|
if (swipeBlock instanceof HTMLElement) {
|
|
const scrollParent = swipeBlock.closest('.swipe_picker_div');
|
|
if (scrollParent instanceof HTMLElement) {
|
|
const blockRect = swipeBlock.getBoundingClientRect();
|
|
const parentRect = scrollParent.getBoundingClientRect();
|
|
if (blockRect.top < parentRect.top) {
|
|
scrollParent.scrollTop -= (parentRect.top - blockRect.top) + 5;
|
|
} else if (blockRect.bottom > parentRect.bottom) {
|
|
scrollParent.scrollTop += (blockRect.bottom - parentRect.bottom) + 5;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function canDeleteSwipeFromPicker(swipeId) {
|
|
if ((message?.swipes?.length ?? 0) <= 1) {
|
|
return false;
|
|
}
|
|
|
|
const currentSwipeId = clamp(Number(message.swipe_id ?? 0), 0, message.swipes.length - 1);
|
|
return canJumpToSwipe || swipeId !== currentSwipeId;
|
|
}
|
|
|
|
async function renderSwipeList() {
|
|
const swipeBlocks = await Promise.all(message.swipes.map(async (swipe, index) => {
|
|
const swipeText = String(swipe ?? '');
|
|
const template = $('#past_chat_template .select_chat_block_wrapper').clone();
|
|
const block = template.find('.select_chat_block');
|
|
block.removeClass('select_chat_block').addClass('swipe_picker_block');
|
|
block.find('.select_chat_actions').removeClass('gap10px');
|
|
const branchButton = template.find('.exportRawChatButton');
|
|
const deleteButton = template.find('.PastChat_cross');
|
|
const swipeInfo = Array.isArray(message.swipe_info) ? message.swipe_info[index] : null;
|
|
const sendDate = swipeInfo?.send_date ? timestampToMoment(swipeInfo.send_date).format('lll') : '';
|
|
const previewText = swipeText.replace(/\s+/g, ' ').trim();
|
|
const tokenCount = swipeInfo?.extra?.token_count ?? await getTokenCountAsync(swipeText, 0);
|
|
const canDeleteSwipe = canDeleteSwipeFromPicker(index);
|
|
const swipeDetails = [];
|
|
|
|
if (previewText) {
|
|
swipeDetails.push(`${previewText.length} ${t`chars`}`);
|
|
}
|
|
|
|
if (tokenCount) {
|
|
swipeDetails.push(`${tokenCount}t`);
|
|
}
|
|
|
|
block.attr({
|
|
file_name: `swipe-${index + 1}`,
|
|
'data-swipe-id': index,
|
|
});
|
|
|
|
template.find('.renameChatButton, .exportChatButton').remove();
|
|
branchButton
|
|
.removeAttr('data-format')
|
|
.attr({
|
|
title: t`Create Branch`,
|
|
'data-i18n': '[title]Create Branch',
|
|
})
|
|
.removeClass('exportRawChatButton fa-solid fa-file-export')
|
|
.addClass('swipe_picker_branch mes_button fa-fw fa-regular fa-code-branch')
|
|
.on('click', async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
setSelectedSwipe(index);
|
|
branchActionSwipeId = index;
|
|
await popup.completeCancelled();
|
|
});
|
|
deleteButton
|
|
.removeAttr('file_name')
|
|
.attr('aria-disabled', String(!canDeleteSwipe))
|
|
.removeClass('fa-skull')
|
|
.addClass('swipe_picker_delete fa-fw fa-trash-can')
|
|
.toggleClass('hoverglow', canDeleteSwipe)
|
|
.toggleClass('disabled', !canDeleteSwipe)
|
|
.each(function () {
|
|
if (canDeleteSwipe) {
|
|
$(this)
|
|
.attr({
|
|
title: t`Delete Swipe`,
|
|
'data-i18n': '[title]Delete Swipe',
|
|
});
|
|
} else {
|
|
$(this)
|
|
.removeAttr('title')
|
|
.removeAttr('data-i18n');
|
|
}
|
|
})
|
|
.off('click')
|
|
.on('click', async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
|
|
if (!canDeleteSwipe) {
|
|
return;
|
|
}
|
|
|
|
const nextSelectedSwipeId = index < selectedSwipeId
|
|
? selectedSwipeId - 1
|
|
: index > selectedSwipeId
|
|
? selectedSwipeId
|
|
: Math.min(selectedSwipeId, message.swipes.length - 2);
|
|
|
|
if (power_user.confirm_message_delete) {
|
|
const result = await callGenericPopup(t`Are you sure you want to delete swipe #${index + 1}?`, POPUP_TYPE.CONFIRM, null, {
|
|
okButton: t`Delete Swipe`,
|
|
cancelButton: t`Cancel`,
|
|
});
|
|
|
|
if (result !== POPUP_RESULT.AFFIRMATIVE) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
const newSwipeId = await deleteSwipe(index, messageId);
|
|
if (!Number.isInteger(newSwipeId)) {
|
|
return;
|
|
}
|
|
|
|
selectedSwipeId = clamp(nextSelectedSwipeId, 0, message.swipes.length - 1);
|
|
|
|
if (swipeIdInput instanceof HTMLInputElement) {
|
|
swipeIdInput.max = String(message.swipes.length);
|
|
}
|
|
|
|
await renderSwipeList();
|
|
});
|
|
|
|
// Add expand/collapse toggle
|
|
const expandCheckboxId = `swipe_picker_expand_${messageId}_${index}`;
|
|
const expandCheckbox = document.createElement('input');
|
|
expandCheckbox.type = 'checkbox';
|
|
expandCheckbox.id = expandCheckboxId;
|
|
expandCheckbox.classList.add('swipe_picker_expand_toggle');
|
|
block[0].prepend(expandCheckbox);
|
|
|
|
const expandLabel = document.createElement('label');
|
|
expandLabel.htmlFor = expandCheckboxId;
|
|
expandLabel.classList.add('swipe_picker_expand_label', 'fa-solid', 'fa-fw', 'fa-chevron-down');
|
|
expandLabel.title = t`Expand/Collapse`;
|
|
expandLabel.setAttribute('data-i18n', '[title]Expand/Collapse');
|
|
expandLabel.addEventListener('click', (event) => event.stopPropagation());
|
|
|
|
// Add copy button
|
|
const copyButton = document.createElement('div');
|
|
copyButton.classList.add('swipe_picker_copy', 'fa-solid', 'fa-fw', 'fa-copy');
|
|
copyButton.title = t`Copy`;
|
|
copyButton.setAttribute('data-i18n', '[title]Copy');
|
|
copyButton.addEventListener('click', async (event) => {
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
await copyText(swipeText);
|
|
toastr.info(t`Copied!`, '', { timeOut: 2000 });
|
|
});
|
|
|
|
// Insert new buttons before the branch button
|
|
branchButton.before(expandLabel, copyButton);
|
|
|
|
template.find('.select_chat_block_filename').text(`#${index + 1}${index === Number(message.swipe_id ?? 0) ? ` ${t`[Current]`}` : ''}`);
|
|
template.find('.chat_messages_date').text(sendDate);
|
|
template.find('.chat_file_size').text(swipeDetails.length ? `(${swipeDetails[0]}${swipeDetails.length > 1 ? ',' : ')'}` : '');
|
|
template.find('.chat_messages_num').text(swipeDetails.length > 1 ? `${swipeDetails.slice(1).join(', ')})` : '');
|
|
template.find('.select_chat_block_mes').text(previewText ? swipeText : t`(empty swipe)`);
|
|
|
|
block.on('click', () => setSelectedSwipe(index));
|
|
block.on('dblclick', async () => {
|
|
if (!canJumpToSwipe) {
|
|
return;
|
|
}
|
|
|
|
setSelectedSwipe(index);
|
|
await popup.completeAffirmative();
|
|
});
|
|
|
|
return template[0];
|
|
}));
|
|
|
|
listContainer.replaceChildren(...swipeBlocks);
|
|
setSelectedSwipe(selectedSwipeId);
|
|
|
|
if (swipeBlocks.length === 0) {
|
|
const empty = document.createElement('div');
|
|
empty.classList.add('textAlignCenter', 'opacity50p', 'padding10');
|
|
empty.textContent = t`No swipes available.`;
|
|
listContainer.replaceChildren(empty);
|
|
}
|
|
}
|
|
|
|
popup = new Popup(wrapper, POPUP_TYPE.CONFIRM, '', {
|
|
okButton: canJumpToSwipe ? t`Go` : false,
|
|
cancelButton: false,
|
|
customInputs: [{
|
|
id: swipeIdInputId,
|
|
label: t`Swipe ID`,
|
|
type: 'text',
|
|
defaultState: String(selectedSwipeId + 1),
|
|
tooltip: `1-${message.swipes.length}`,
|
|
}],
|
|
large: true,
|
|
wider: true,
|
|
allowVerticalScrolling: true,
|
|
onOpen: function () {
|
|
scrollToSelectedSwipe();
|
|
if (swipeIdInput instanceof HTMLInputElement) {
|
|
swipeIdInput.focus();
|
|
swipeIdInput.select();
|
|
}
|
|
},
|
|
onClosing: function (popup) {
|
|
if (popup.result !== POPUP_RESULT.AFFIRMATIVE) {
|
|
return true;
|
|
}
|
|
|
|
const swipeIdInput = popup.dlg.querySelector(`#${swipeIdInputId}`);
|
|
const targetSwipeNumber = Number.parseInt(String(swipeIdInput instanceof HTMLInputElement ? swipeIdInput.value : '').trim(), 10);
|
|
|
|
if (!Number.isInteger(targetSwipeNumber) || targetSwipeNumber < 1 || targetSwipeNumber > message.swipes.length) {
|
|
toastr.warning(t`Enter a swipe ID between 1 and ${message.swipes.length}.`, t`Jump to Swipe`);
|
|
if (swipeIdInput instanceof HTMLInputElement) {
|
|
swipeIdInput.focus();
|
|
swipeIdInput.select();
|
|
}
|
|
return false;
|
|
}
|
|
|
|
setSelectedSwipe(targetSwipeNumber - 1);
|
|
return true;
|
|
},
|
|
});
|
|
|
|
popup.dlg.classList.add('swipe_picker_popup');
|
|
popup.closeButton.style.display = 'block';
|
|
popup.closeButton.classList.add('opacity50p', 'hoverglow', 'fontsize120p');
|
|
popup.closeButton.style.position = 'static';
|
|
popup.closeButton.style.top = 'auto';
|
|
popup.closeButton.style.right = 'auto';
|
|
popup.closeButton.style.width = 'auto';
|
|
popup.closeButton.style.height = 'auto';
|
|
popup.closeButton.style.padding = '0';
|
|
popup.closeButton.style.filter = 'none';
|
|
header.appendChild(popup.closeButton);
|
|
|
|
swipeIdInput = popup.dlg.querySelector(`#${swipeIdInputId}`);
|
|
const swipeIdLabel = popup.dlg.querySelector(`label[for="${swipeIdInputId}"]`);
|
|
|
|
if (swipeIdLabel instanceof HTMLLabelElement) {
|
|
swipeIdLabel.classList.add('flex-container', 'alignItemsCenter', 'justifyCenter', 'gap10px', 'margin0');
|
|
popup.buttonControls.insertBefore(swipeIdLabel, canJumpToSwipe ? popup.okButton : popup.buttonControls.firstChild);
|
|
popup.inputControls.style.display = 'none';
|
|
}
|
|
|
|
if (swipeIdInput instanceof HTMLInputElement) {
|
|
swipeIdInput.type = 'number';
|
|
swipeIdInput.min = '1';
|
|
swipeIdInput.max = String(message.swipes.length);
|
|
swipeIdInput.step = '1';
|
|
swipeIdInput.inputMode = 'numeric';
|
|
swipeIdInput.classList.add('flex1', 'width100px', 'textAlignCenter');
|
|
swipeIdInput.setAttribute('autofocus', '');
|
|
syncSwipeIdInput();
|
|
|
|
swipeIdInput.addEventListener('input', function () {
|
|
const nextSwipeId = Number.parseInt(this.value, 10);
|
|
if (!Number.isInteger(nextSwipeId) || nextSwipeId < 1 || nextSwipeId > message.swipes.length) {
|
|
return;
|
|
}
|
|
|
|
setSelectedSwipe(nextSwipeId - 1);
|
|
scrollToSelectedSwipe();
|
|
});
|
|
|
|
swipeIdInput.addEventListener('blur', function () {
|
|
syncSwipeIdInput();
|
|
});
|
|
}
|
|
|
|
await renderSwipeList();
|
|
|
|
const popupResult = await popup.show();
|
|
|
|
if (branchActionSwipeId !== null) {
|
|
await branchChat(messageId, { swipeId: branchActionSwipeId });
|
|
return;
|
|
}
|
|
|
|
if (popupResult !== POPUP_RESULT.AFFIRMATIVE) {
|
|
return;
|
|
}
|
|
|
|
if (!canJumpToSwipe) {
|
|
return;
|
|
}
|
|
|
|
const targetSwipeId = clamp(selectedSwipeId, 0, message.swipes.length - 1);
|
|
const currentSwipeId = clamp(Number(message.swipe_id ?? 0), 0, message.swipes.length - 1);
|
|
|
|
if (targetSwipeId === currentSwipeId) {
|
|
toastr.info(t`Already showing swipe #${targetSwipeId + 1}.`, t`Jump to Swipe`);
|
|
return;
|
|
}
|
|
|
|
const direction = targetSwipeId > currentSwipeId ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT;
|
|
await swipe(null, direction, { source: SWIPE_SOURCE.SWIPE_PICKER, forceMesId: messageId, forceSwipeId: targetSwipeId });
|
|
}
|
|
|
|
export function initSwipePicker() {
|
|
/**
|
|
* Click handler for opening the swipe picker when clicking on the swipe counter.
|
|
* @param {JQuery.Event | Event} e Event object
|
|
*/
|
|
async function onSwipeCounterClick(e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const mesId = Number($(this).closest('.mes').attr('mesid'));
|
|
await openSwipePicker(mesId);
|
|
}
|
|
|
|
if (isMobile()) {
|
|
addLongPressEvent('.swipes-counter.swipe-picker-enabled', onSwipeCounterClick);
|
|
} else {
|
|
$(document).on('click', '.swipes-counter.swipe-picker-enabled', onSwipeCounterClick);
|
|
}
|
|
$(document).on('keydown', '.swipes-counter.swipe-picker-enabled', async function (e) {
|
|
if (e.key !== ' ') {
|
|
return;
|
|
}
|
|
|
|
onSwipeCounterClick.call(this, e);
|
|
});
|
|
$(document).on('click', '.mes_swipe_picker', async function (e) {
|
|
e.preventDefault();
|
|
e.stopPropagation();
|
|
|
|
const mesId = Number($(this).closest('.mes').attr('mesid'));
|
|
await openSwipePicker(mesId);
|
|
});
|
|
}
|