Image swipe is back 🗣️🔥

73cedc115dbd82dff891acb877deb5494f8e653d

Cohee <18619528+Cohee1207@users.noreply.github.com>

5 files changed, +86 -26Showing whitespace changes
public/index.html+9 -0
@@ -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">
public/scripts/chats.js+7 -19
@@ -2080,24 +2080,6 @@ async function onImageSwiped(messageId, element, direction) {
2080 return;2080 return;
2081 }2081 }
20822082
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);
21032085
@@ -2106,6 +2088,13 @@ async function onImageSwiped(messageId, element, direction) {
2106 return;2088 return;
2107 }2089 }
21082090
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 beginning2098 // 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 }
21202109
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}
21252113
public/scripts/constants.js+9 -0
@@ -88,6 +88,15 @@ export const MEDIA_DISPLAY = {
88};88};
8989
90/**90/**
91 * @enum {string}
92 * @readonly
93 */
94export const IMAGE_OVERSWIPE = {
95 GENERATE: 'generate',
96 ROLLOVER: 'rollover',
97};
98
99/**
91 * @readonly100 * @readonly
92 */101 */
93export const MEDIA_TYPE = {102export const MEDIA_TYPE = {
public/scripts/extensions/stable-diffusion/index.js+53 -6
@@ -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';
55import { debounce_timeout, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, VIDEO_EXTENSIONS } from '../../constants.js';55import { debounce_timeout, IMAGE_OVERSWIPE, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION, VIDEO_EXTENSIONS } from '../../constants.js';
56import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';56import { SlashCommandEnumValue } from '../../slash-commands/SlashCommandEnumValue.js';
57import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';57import { callGenericPopup, Popup, POPUP_TYPE } from '../../popup.js';
58import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';58import { commonEnumProviders } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
@@ -4238,7 +4238,7 @@ async function addSDGenButtons() {
4238 placement: 'top',4238 placement: 'top',
4239 });4239 });
42404240
4241 $(document).on('click', '.sd_message_gen', sdMessageButton);4241 $(document).on('click', '.sd_message_gen', (e) => sdMessageButton($(e.currentTarget), { animate: false }));
42424242
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;
43264326
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 */
4332async function sdMessageButton(e) {4334async 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 }
43414344
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);
43454349
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 };
43754379
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}
44834492
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 */
4500async 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} NamedArguments4532 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArguments} NamedArguments
4487 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture4533 * @typedef {import('../../slash-commands/SlashCommand.js').NamedArgumentsCapture} NamedArgumentsCapture
@@ -4972,6 +5018,7 @@ jQuery(async () => {
4972 });5018 });
49735019
4974 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);5020 eventSource.on(event_types.CHAT_CHANGED, onChatChanged);
5021 eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped);
49755022
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) => {
public/scripts/power-user.js+8 -1
@@ -63,7 +63,7 @@ import { fuzzySearchCategories } from './filters.js';
63import { accountStorage } from './util/AccountStorage.js';63import { accountStorage } from './util/AccountStorage.js';
64import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';64import { DEFAULT_REASONING_TEMPLATE, loadReasoningTemplates } from './reasoning.js';
65import { bindModelTemplates } from './chat-templates.js';65import { bindModelTemplates } from './chat-templates.js';
66import { MEDIA_DISPLAY } from './constants.js';66import { IMAGE_OVERSWIPE, MEDIA_DISPLAY } from './constants.js';
67import { t } from './i18n.js';67import { t } from './i18n.js';
6868
69export const toastPositionClasses = [69export 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};
345346
346let themes = [];347let 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);
17771779
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 });
41924194
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);