Image swipe is back 🗣️🔥
| @@ -5086,6 +5086,15 @@ | |||
| 5086 | <option value="1" data-i18n="Enabled">Enabled</option> | 5086 | <option value="1" data-i18n="Enabled">Enabled</option> |
| 5087 | </select> | 5087 | </select> |
| 5088 | </div> | 5088 | </div> |
| 5089 | <div class="flex-container alignItemsCenter"> | ||
| 5090 | <small data-i18n="Image Swipe Behavior:"> | ||
| 5091 | Image Swipe Behavior: | ||
| 5092 | </small> | ||
| 5093 | <select id="image_overswipe" class="widthNatural flex1 margin0"> | ||
| 5094 | <option value="generate" data-i18n="Generate new">Generate new</option> | ||
| 5095 | <option value="rollover" data-i18n="Roll over">Roll over</option> | ||
| 5096 | </select> | ||
| 5097 | </div> | ||
| 5089 | <label class="checkbox_label" for="continue_on_send"> | 5098 | <label class="checkbox_label" for="continue_on_send"> |
| 5090 | <input id="continue_on_send" type="checkbox" /> | 5099 | <input id="continue_on_send" type="checkbox" /> |
| 5091 | <small data-i18n="Press Send to continue"> | 5100 | <small data-i18n="Press Send to continue"> |
| @@ -2080,24 +2080,6 @@ async function onImageSwiped(messageId, element, direction) { | |||
| 2080 | return; | 2080 | return; |
| 2081 | } | 2081 | } |
| 2082 | 2082 | ||
| 2083 | // Show a message that swipe right no longer automatically generates an image | ||
| 2084 | if (direction === SWIPE_DIRECTION.RIGHT && message.extra.media_index === media.length - 1) { | ||
| 2085 | const key = 'imageSwipeNoticeShown'; | ||
| 2086 | const hasSeenNotice = accountStorage.getItem(key); | ||
| 2087 | if (!hasSeenNotice) { | ||
| 2088 | await Popup.show.text( | ||
| 2089 | t`Image swiping no longer automatically generates new images.`, | ||
| 2090 | t`Use the 'Generate Image' (paintbrush) button in the message actions menu to generate more images. This message will not be shown again.`, | ||
| 2091 | ); | ||
| 2092 | accountStorage.setItem(key, 'true'); | ||
| 2093 | } | ||
| 2094 | } | ||
| 2095 | |||
| 2096 | if (media.length === 1) { | ||
| 2097 | console.warn('Only one media item in the message, swiping is not applicable'); | ||
| 2098 | return; | ||
| 2099 | } | ||
| 2100 | |||
| 2101 | const currentIndex = getMediaIndex(message); | 2083 | const currentIndex = getMediaIndex(message); |
| 2102 | const mediaDisplay = getMediaDisplay(message); | 2084 | const mediaDisplay = getMediaDisplay(message); |
| 2103 | 2085 | ||
| @@ -2106,6 +2088,13 @@ async function onImageSwiped(messageId, element, direction) { | |||
| 2106 | return; | 2088 | return; |
| 2107 | } | 2089 | } |
| 2108 | 2090 | ||
| 2091 | await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction }); | ||
| 2092 | |||
| 2093 | if (media.length === 1) { | ||
| 2094 | console.warn('Only one media item in the message, swiping is not applicable'); | ||
| 2095 | return; | ||
| 2096 | } | ||
| 2097 | |||
| 2109 | // Switch to previous image or wrap around if at the beginning | 2098 | // Switch to previous image or wrap around if at the beginning |
| 2110 | if (direction === SWIPE_DIRECTION.LEFT) { | 2099 | if (direction === SWIPE_DIRECTION.LEFT) { |
| 2111 | const newIndex = currentIndex === 0 ? media.length - 1 : currentIndex - 1; | 2100 | const newIndex = currentIndex === 0 ? media.length - 1 : currentIndex - 1; |
| @@ -2119,7 +2108,6 @@ async function onImageSwiped(messageId, element, direction) { | |||
| 2119 | } | 2108 | } |
| 2120 | 2109 | ||
| 2121 | await saveChatConditional(); | 2110 | await saveChatConditional(); |
| 2122 | await eventSource.emit(event_types.IMAGE_SWIPED, { message, element, direction }); | ||
| 2123 | appendMediaToMessage(message, element); | 2111 | appendMediaToMessage(message, element); |
| 2124 | } | 2112 | } |
| 2125 | 2113 | ||
| @@ -88,6 +88,15 @@ export const MEDIA_DISPLAY = { | |||
| 88 | }; | 88 | }; |
| 89 | 89 | ||
| 90 | /** | 90 | /** |
| 91 | * @enum {string} | ||
| 92 | * @readonly | ||
| 93 | */ | ||
| 94 | export const IMAGE_OVERSWIPE = { | ||
| 95 | GENERATE: 'generate', | ||
| 96 | ROLLOVER: 'rollover', | ||
| 97 | }; | ||
| 98 | |||
| 99 | /** | ||
| 91 | * @readonly | 100 | * @readonly |
| 92 | */ | 101 | */ |
| 93 | export const MEDIA_TYPE = { | 102 | export const MEDIA_TYPE = { |
| @@ -52,7 +52,7 @@ import { | |||
| 52 | SlashCommandArgument, | 52 | SlashCommandArgument, |
| 53 | SlashCommandNamedArgument, | 53 | SlashCommandNamedArgument, |
| 54 | } from '../../slash-commands/SlashCommandArgument.js'; | 54 | } from '../../slash-commands/SlashCommandArgument.js'; |
| 55 | import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, VIDEO_EXTENSIONS } from '../../constants.js'; | 55 | import { debounce_timeout, IMAGE_OVERSWIPE, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION, VIDEO_EXTENSIONS } from '../../constants.js'; |
| 56 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; | 56 | import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js'; |
| 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; | 57 | import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js'; |
| 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; | 58 | import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js'; |
| @@ -4238,7 +4238,7 @@ async function addSDGenButtons() { | |||
| 4238 | placement: 'top', | 4238 | placement: 'top', |
| 4239 | }); | 4239 | }); |
| 4240 | 4240 | ||
| 4241 | $(document).on('click', '.sd_message_gen', sdMessageButton); | 4241 | $(document).on('click', '.sd_message_gen', (e) => sdMessageButton($(e.currentTarget), { animate: false })); |
| 4242 | 4242 | ||
| 4243 | $(document).on('click touchend', function (e) { | 4243 | $(document).on('click touchend', function (e) { |
| 4244 | const target = $(e.target); | 4244 | const target = $(e.target); |
| @@ -4326,10 +4326,12 @@ let buttonAbortController = null; | |||
| 4326 | 4326 | ||
| 4327 | /** | 4327 | /** |
| 4328 | * "Paintbrush" button handler to generate a new image for a message. | 4328 | * "Paintbrush" button handler to generate a new image for a message. |
| 4329 | * @param {JQuery.ClickEvent} e The click event object. | 4329 | * @param {JQuery<HTMLElement>} $icon The click target. |
| 4330 | * @param {Object} [options] Additional options for image generation. | ||
| 4331 | * @param {boolean} [options.animate] Whether to animate the media during generation. | ||
| 4330 | * @returns {Promise<void>} A promise that resolves when the image generation process is complete. | 4332 | * @returns {Promise<void>} A promise that resolves when the image generation process is complete. |
| 4331 | */ | 4333 | */ |
| 4332 | async function sdMessageButton(e) { | 4334 | async function sdMessageButton($icon, { animate } = {}) { |
| 4333 | /** | 4335 | /** |
| 4334 | * Sets the icon to indicate busy or idle state. | 4336 | * Sets the icon to indicate busy or idle state. |
| 4335 | * @param {boolean} isBusy Whether the icon should indicate a busy state. | 4337 | * @param {boolean} isBusy Whether the icon should indicate a busy state. |
| @@ -4337,11 +4339,13 @@ async function sdMessageButton(e) { | |||
| 4337 | function setBusyIcon(isBusy) { | 4339 | function setBusyIcon(isBusy) { |
| 4338 | $icon.toggleClass(classes.idle, !isBusy); | 4340 | $icon.toggleClass(classes.idle, !isBusy); |
| 4339 | $icon.toggleClass(classes.busy, isBusy); | 4341 | $icon.toggleClass(classes.busy, isBusy); |
| 4342 | $media.toggleClass(classes.animation, isBusy); | ||
| 4340 | } | 4343 | } |
| 4341 | 4344 | ||
| 4342 | const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush' }; | 4345 | let $media = jQuery(); |
| 4346 | |||
| 4347 | const classes = { busy: 'fa-hourglass', idle: 'fa-paintbrush', animation: 'fa-fade' }; | ||
| 4343 | const context = getContext(); | 4348 | const context = getContext(); |
| 4344 | const $icon = $(e.currentTarget); | ||
| 4345 | 4349 | ||
| 4346 | if ($icon.hasClass(classes.busy)) { | 4350 | if ($icon.hasClass(classes.busy)) { |
| 4347 | buttonAbortController?.abort('Aborted by user'); | 4351 | buttonAbortController?.abort('Aborted by user'); |
| @@ -4373,6 +4377,11 @@ async function sdMessageButton(e) { | |||
| 4373 | ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1]) | 4377 | ? (message.extra.media[message.extra.media_index] ?? message.extra.media[message.extra.media.length - 1]) |
| 4374 | : { url: '', title: message.mes, type: MEDIA_TYPE.IMAGE, generation_type: generationMode.FREE }; | 4378 | : { url: '', title: message.mes, type: MEDIA_TYPE.IMAGE, generation_type: generationMode.FREE }; |
| 4375 | 4379 | ||
| 4380 | if (animate && message.extra.media.length > 0) { | ||
| 4381 | const index = message.extra.media.indexOf(selectedMedia); | ||
| 4382 | $media = messageElement.find(`.mes_media_container[data-index="${index}"]`).find('.mes_img, .mes_video'); | ||
| 4383 | } | ||
| 4384 | |||
| 4376 | buttonAbortController = new AbortController(); | 4385 | buttonAbortController = new AbortController(); |
| 4377 | const newMediaAttachment = await generateMediaSwipe( | 4386 | const newMediaAttachment = await generateMediaSwipe( |
| 4378 | selectedMedia, | 4387 | selectedMedia, |
| @@ -4482,6 +4491,43 @@ async function generateMediaSwipe(mediaAttachment, message, onStart, onComplete, | |||
| 4482 | } | 4491 | } |
| 4483 | 4492 | ||
| 4484 | /** | 4493 | /** |
| 4494 | * Handles the image swipe event to potentially generate a new image. | ||
| 4495 | * @param {object} param Parameters object | ||
| 4496 | * @param {ChatMessage} param.message Message object | ||
| 4497 | * @param {JQuery<HTMLElement>} param.element Message element | ||
| 4498 | * @param {string} param.direction Swipe direction | ||
| 4499 | */ | ||
| 4500 | async function onImageSwiped({ message, element, direction }) { | ||
| 4501 | const { powerUserSettings, accountStorage } = getContext(); | ||
| 4502 | |||
| 4503 | if (!message || direction !== SWIPE_DIRECTION.RIGHT || powerUserSettings.image_overswipe !== IMAGE_OVERSWIPE.GENERATE) { | ||
| 4504 | return; | ||
| 4505 | } | ||
| 4506 | |||
| 4507 | const media = message?.extra?.media; | ||
| 4508 | if (!Array.isArray(media) || media.length === 0) { | ||
| 4509 | return; | ||
| 4510 | } | ||
| 4511 | |||
| 4512 | const shouldGenerate = message?.extra?.media_index === media.length - 1; | ||
| 4513 | if (!shouldGenerate) { | ||
| 4514 | return; | ||
| 4515 | } | ||
| 4516 | |||
| 4517 | const key = 'imageSwipeNoticeShown'; | ||
| 4518 | const hasSeenNotice = accountStorage.getItem(key); | ||
| 4519 | if (!hasSeenNotice) { | ||
| 4520 | await Popup.show.text( | ||
| 4521 | t`Image Generation Notice`, | ||
| 4522 | t`To disable generation on image swipes, change the "Image Swipe Behavior" setting in the User Settings panel. This message will not be shown again.`, | ||
| 4523 | ); | ||
| 4524 | accountStorage.setItem(key, 'true'); | ||
| 4525 | } | ||
| 4526 | |||
| 4527 | await sdMessageButton(element.find('.sd_message_gen'), { animate: true }); | ||
| 4528 | } | ||
| 4529 | |||
| 4530 | /** | ||
| 4485 | * Applies the command arguments to the extension settings. | 4531 | * Applies the command arguments to the extension settings. |
| 4486 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments | 4532 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments |
| 4487 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture | 4533 | * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture |
| @@ -4972,6 +5018,7 @@ jQuery(async () => { | |||
| 4972 | }); | 5018 | }); |
| 4973 | 5019 | ||
| 4974 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); | 5020 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 5021 | eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped); | ||
| 4975 | 5022 | ||
| 4976 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { | 5023 | [event_types.SECRET_WRITTEN, event_types.SECRET_DELETED, event_types.SECRET_ROTATED].forEach(event => { |
| 4977 | eventSource.on(event, async (/** @type {string} */ key) => { | 5024 | eventSource.on(event, async (/** @type {string} */ key) => { |
| @@ -63,7 +63,7 @@ import { fuzzySearchCategories } from './filters.js'; | |||
| 63 | import { accountStorage } from './util/AccountStorage.js'; | 63 | import { accountStorage } from './util/AccountStorage.js'; |
| 64 | import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js'; | 64 | import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js'; |
| 65 | import { bindModelTemplates } from './chat-templates.js'; | 65 | import { bindModelTemplates } from './chat-templates.js'; |
| 66 | import { MEDIA_DISPLAY } from './constants.js'; | 66 | import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js'; |
| 67 | import { t } from './i18n.js'; | 67 | import { t } from './i18n.js'; |
| 68 | 68 | ||
| 69 | export const toastPositionClasses = [ | 69 | export const toastPositionClasses = [ |
| @@ -341,6 +341,7 @@ export const power_user = { | |||
| 341 | pin_styles: true, | 341 | pin_styles: true, |
| 342 | click_to_edit: false, | 342 | click_to_edit: false, |
| 343 | media_display: MEDIA_DISPLAY.LIST, | 343 | media_display: MEDIA_DISPLAY.LIST, |
| 344 | image_overswipe: IMAGE_OVERSWIPE.GENERATE, | ||
| 344 | }; | 345 | }; |
| 345 | 346 | ||
| 346 | let themes = []; | 347 | let themes = []; |
| @@ -1774,6 +1775,7 @@ export async function loadPowerUserSettings(settings, data) { | |||
| 1774 | $('#pin_styles').prop('checked', power_user.pin_styles); | 1775 | $('#pin_styles').prop('checked', power_user.pin_styles); |
| 1775 | $('#click_to_edit').prop('checked', power_user.click_to_edit); | 1776 | $('#click_to_edit').prop('checked', power_user.click_to_edit); |
| 1776 | $('#media_display').val(power_user.media_display); | 1777 | $('#media_display').val(power_user.media_display); |
| 1778 | $('#image_overswipe').val(power_user.image_overswipe); | ||
| 1777 | 1779 | ||
| 1778 | for (const theme of themes) { | 1780 | for (const theme of themes) { |
| 1779 | const option = document.createElement('option'); | 1781 | const option = document.createElement('option'); |
| @@ -4190,6 +4192,11 @@ jQuery(() => { | |||
| 4190 | } | 4192 | } |
| 4191 | }); | 4193 | }); |
| 4192 | 4194 | ||
| 4195 | $('#image_overswipe').on('input', function () { | ||
| 4196 | power_user.image_overswipe = $(this).val().toString(); | ||
| 4197 | saveSettingsDebounced(); | ||
| 4198 | }); | ||
| 4199 | |||
| 4193 | $(document).on('click', '#debug_table [data-debug-function]', function () { | 4200 | $(document).on('click', '#debug_table [data-debug-function]', function () { |
| 4194 | const functionId = $(this).data('debug-function'); | 4201 | const functionId = $(this).data('debug-function'); |
| 4195 | const functionRecord = debug_functions.find(f => f.functionId === functionId); | 4202 | const functionRecord = debug_functions.find(f => f.functionId === functionId); |