Image Generation: Add swipes for generated images Supersedes #2648
| @@ -5801,6 +5801,11 @@ | ||
| 5801 | 5801 | <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div> |
| 5802 | 5802 | <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div> |
| 5803 | 5803 | </div> |
| 5804 | + <div class="mes_img_swipes"> | |
| 5805 | + <div title="Swipe left" class="right_menu_button fa-lg fa-solid fa-chevron-left mes_img_swipe_left" data-i18n="[title]Swipe left"></div> | |
| 5806 | + <div class="mes_img_swipe_counter">1/1</div> | |
| 5807 | + <div title="Swipe right" class="right_menu_button fa-lg fa-solid fa-chevron-right mes_img_swipe_right" data-i18n="[title]Swipe right"></div> | |
| 5808 | + </div> | |
| 5804 | 5809 | <img class="mes_img" src="" /> |
| 5805 | 5810 | </div> |
| 5806 | 5811 | <div class="mes_bias"></div> |
| @@ -459,6 +459,7 @@ export const event_types = { | ||
| 459 | 459 | LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register', |
| 460 | 460 | LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', |
| 461 | 461 | ONLINE_STATUS_CHANGED: 'online_status_changed', |
| 462 | + IMAGE_SWIPED: 'image_swiped', | |
| 462 | 463 | }; |
| 463 | 464 | |
| 464 | 465 | export const eventSource = new EventEmitter(); |
| @@ -2112,6 +2113,7 @@ export function updateMessageBlock(messageId, message) { | ||
| 2112 | 2113 | export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { |
| 2113 | 2114 | // Add image to message |
| 2114 | 2115 | if (mes.extra?.image) { |
| 2116 | + const container = messageElement.find('.mes_img_container'); | |
| 2115 | 2117 | const chatHeight = $('#chat').prop('scrollHeight'); |
| 2116 | 2118 | const image = messageElement.find('.mes_img'); |
| 2117 | 2119 | const text = messageElement.find('.mes_text'); |
| @@ -2127,9 +2129,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | ||
| 2127 | 2129 | }); |
| 2128 | 2130 | image.attr('src', mes.extra?.image); |
| 2129 | 2131 | image.attr('title', mes.extra?.title || mes.title || ''); |
| 2130 | 2132 | messageElement.find('.mes_img_container')container.addClass('img_extra'); |
| 2131 | 2133 | image.toggleClass('img_inline', isInline); |
| 2132 | 2134 | text.toggleClass('displayNone', !isInline); |
| 2135 | + | |
| 2136 | + const imageSwipes = mes.extra.image_swipes; | |
| 2137 | + if (Array.isArray(imageSwipes) && imageSwipes.length > 0) { | |
| 2138 | + container.addClass('img_swipes'); | |
| 2139 | + const counter = container.find('.mes_img_swipe_counter'); | |
| 2140 | + const currentImage = imageSwipes.indexOf(mes.extra.image) + 1; | |
| 2141 | + counter.text(`${currentImage}/${imageSwipes.length}`); | |
| 2142 | + | |
| 2143 | + const swipeLeft = container.find('.mes_img_swipe_left'); | |
| 2144 | + swipeLeft.off('click').on('click', function () { | |
| 2145 | + eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'left' }); | |
| 2146 | + }); | |
| 2147 | + | |
| 2148 | + const swipeRight = container.find('.mes_img_swipe_right'); | |
| 2149 | + swipeRight.off('click').on('click', function () { | |
| 2150 | + eventSource.emit(event_types.IMAGE_SWIPED, { message: mes, element: messageElement, direction: 'right' }); | |
| 2151 | + }); | |
| 2152 | + } | |
| 2133 | 2153 | } |
| 2134 | 2154 | |
| 2135 | 2155 | // Add file to message |
| @@ -59,6 +59,7 @@ const initiators = { | ||
| 59 | 59 | action: 'action', |
| 60 | 60 | interactive: 'interactive', |
| 61 | 61 | wand: 'wand', |
| 62 | + swipe: 'swipe', | |
| 62 | 63 | }; |
| 63 | 64 | |
| 64 | 65 | const generationMode = { |
| @@ -3429,6 +3430,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref | ||
| 3429 | 3430 | generationType: generationType, |
| 3430 | 3431 | negative: additionalNegativePrefix, |
| 3431 | 3432 | inline_image: false, |
| 3433 | + image_swipes: [image], | |
| 3432 | 3434 | }, |
| 3433 | 3435 | }; |
| 3434 | 3436 | context.chat.push(message); |
| @@ -3657,6 +3659,99 @@ async function writePromptFields(characterId) { | ||
| 3657 | 3659 | await writeExtensionField(characterId, 'sd_character_prompt', promptObject); |
| 3658 | 3660 | } |
| 3659 | 3661 | |
| 3662 | +/** | |
| 3663 | + * Switches an image to the next or previous one in the swipe list. | |
| 3664 | + * @param {object} args Event arguments | |
| 3665 | + * @param {any} args.message Message object | |
| 3666 | + * @param {JQuery<HTMLElement>} args.element Message element | |
| 3667 | + * @param {string} args.direction Swipe direction | |
| 3668 | + * @returns {Promise<void>} | |
| 3669 | + */ | |
| 3670 | +async function onImageSwiped({ message, element, direction }) { | |
| 3671 | + const context = getContext(); | |
| 3672 | + const animationClass = 'fa-fade'; | |
| 3673 | + const messageImg = element.find('.mes_img'); | |
| 3674 | + | |
| 3675 | + // Current image is already animating | |
| 3676 | + if (messageImg.hasClass(animationClass)) { | |
| 3677 | + return; | |
| 3678 | + } | |
| 3679 | + | |
| 3680 | + const swipes = message?.extra?.image_swipes; | |
| 3681 | + | |
| 3682 | + if (!Array.isArray(swipes)) { | |
| 3683 | + console.warn('No image swipes found in the message'); | |
| 3684 | + return; | |
| 3685 | + } | |
| 3686 | + | |
| 3687 | + const currentIndex = swipes.indexOf(message.extra.image); | |
| 3688 | + | |
| 3689 | + if (currentIndex === -1) { | |
| 3690 | + console.warn('Current image not found in the swipes'); | |
| 3691 | + return; | |
| 3692 | + } | |
| 3693 | + | |
| 3694 | + // Switch to previous image or wrap around if at the beginning | |
| 3695 | + if (direction === 'left') { | |
| 3696 | + const newIndex = currentIndex === 0 ? swipes.length - 1 : currentIndex - 1; | |
| 3697 | + message.extra.image = swipes[newIndex]; | |
| 3698 | + | |
| 3699 | + // Update the image in the message | |
| 3700 | + appendMediaToMessage(message, element, false); | |
| 3701 | + } | |
| 3702 | + | |
| 3703 | + // Switch to next image or generate a new one if at the end | |
| 3704 | + if (direction === 'right') { | |
| 3705 | + const newIndex = currentIndex === swipes.length - 1 ? swipes.length : currentIndex + 1; | |
| 3706 | + | |
| 3707 | + if (newIndex === swipes.length) { | |
| 3708 | + const abortController = new AbortController(); | |
| 3709 | + const swipeControls = element.find('.mes_img_swipes'); | |
| 3710 | + const stopButton = document.getElementById('sd_stop_gen'); | |
| 3711 | + const stopListener = () => abortController.abort('Aborted by user'); | |
| 3712 | + const generationType = message?.extra?.generationType ?? generationMode.FREE; | |
| 3713 | + const dimensions = setTypeSpecificDimensions(generationType); | |
| 3714 | + const originalSeed = extension_settings.sd.seed; | |
| 3715 | + extension_settings.sd.seed = Math.round(Math.random() * Number.MAX_SAFE_INTEGER); | |
| 3716 | + let imagePath = ''; | |
| 3717 | + | |
| 3718 | + try { | |
| 3719 | + $(stopButton).show(); | |
| 3720 | + eventSource.once(CUSTOM_STOP_EVENT, stopListener); | |
| 3721 | + const callback = () => { }; | |
| 3722 | + const hasNegative = message.extra.negative; | |
| 3723 | + const prompt = await refinePrompt(message.extra.title, false, false); | |
| 3724 | + const negativePromptPrefix = hasNegative ? await refinePrompt(message.extra.negative, false, true) : ''; | |
| 3725 | + const characterName = context.characterId | |
| 3726 | + ? context.characters[context.characterId].name | |
| 3727 | + : context.groups[Object.keys(context.groups).filter(x => context.groups[x].id === context.groupId)[0]]?.id?.toString(); | |
| 3728 | + | |
| 3729 | + messageImg.addClass(animationClass); | |
| 3730 | + swipeControls.hide(); | |
| 3731 | + imagePath = await sendGenerationRequest(generationType, prompt, negativePromptPrefix, characterName, callback, initiators.swipe, abortController.signal); | |
| 3732 | + } finally { | |
| 3733 | + $(stopButton).hide(); | |
| 3734 | + messageImg.removeClass(animationClass); | |
| 3735 | + swipeControls.show(); | |
| 3736 | + eventSource.removeListener(CUSTOM_STOP_EVENT, stopListener); | |
| 3737 | + restoreOriginalDimensions(dimensions); | |
| 3738 | + extension_settings.sd.seed = originalSeed; | |
| 3739 | + } | |
| 3740 | + | |
| 3741 | + if (!imagePath) { | |
| 3742 | + return; | |
| 3743 | + } | |
| 3744 | + | |
| 3745 | + swipes.push(imagePath); | |
| 3746 | + } | |
| 3747 | + | |
| 3748 | + message.extra.image = swipes[newIndex]; | |
| 3749 | + appendMediaToMessage(message, element, false); | |
| 3750 | + } | |
| 3751 | + | |
| 3752 | + await context.saveChat(); | |
| 3753 | +} | |
| 3754 | + | |
| 3660 | 3755 | jQuery(async () => { |
| 3661 | 3756 | await addSDGenButtons(); |
| 3662 | 3757 | |
| @@ -3795,6 +3890,8 @@ jQuery(async () => { | ||
| 3795 | 3890 | } |
| 3796 | 3891 | }); |
| 3797 | 3892 | |
| 3893 | + eventSource.on(event_types.IMAGE_SWIPED, onImageSwiped); | |
| 3894 | + | |
| 3798 | 3895 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 3799 | 3896 | |
| 3800 | 3897 | await loadSettings(); |
| @@ -4569,6 +4569,7 @@ a { | ||
| 4569 | 4569 | image-rendering: -webkit-optimize-contrast; |
| 4570 | 4570 | } |
| 4571 | 4571 | |
| 4572 | +.mes_img_swipes, | |
| 4572 | 4573 | .mes_img_controls { |
| 4573 | 4574 | position: absolute; |
| 4574 | 4575 | top: 0.1em; |
| @@ -4578,9 +4579,16 @@ a { | ||
| 4578 | 4579 | opacity: 0; |
| 4579 | 4580 | flex-direction: row; |
| 4580 | 4581 | justify-content: space-between; |
| 4582 | + align-items: center; | |
| 4581 | 4583 | padding: 1em; |
| 4582 | 4584 | } |
| 4583 | 4585 | |
| 4586 | +.mes_img_swipes { | |
| 4587 | + top: unset; | |
| 4588 | + bottom: 0.1rem; | |
| 4589 | +} | |
| 4590 | + | |
| 4591 | +.mes_img_swipes .right_menu_button, | |
| 4584 | 4592 | .mes_img_controls .right_menu_button { |
| 4585 | 4593 | filter: brightness(90%); |
| 4586 | 4594 | text-shadow: 1px 1px var(--SmartThemeShadowColor) !important; |
| @@ -4589,16 +4597,20 @@ a { | ||
| 4589 | 4597 | width: 1.25em; |
| 4590 | 4598 | } |
| 4591 | 4599 | |
| 4600 | +.mes_img_swipes .right_menu_button::before, | |
| 4592 | 4601 | .mes_img_controls .right_menu_button::before { |
| 4593 | 4602 | /* Fix weird alignment with this font-awesome icons on focus */ |
| 4594 | 4603 | position: relative; |
| 4595 | 4604 | top: 0.6125em; |
| 4596 | 4605 | } |
| 4597 | 4606 | |
| 4607 | +.mes_img_swipes .right_menu_button:hover, | |
| 4598 | 4608 | .mes_img_controls .right_menu_button:hover { |
| 4599 | 4609 | filter: brightness(150%); |
| 4600 | 4610 | } |
| 4601 | 4611 | |
| 4612 | +.mes_img_container:hover .mes_img_swipes, | |
| 4613 | +.mes_img_container:focus-within .mes_img_swipes, | |
| 4602 | 4614 | .mes_img_container:hover .mes_img_controls, |
| 4603 | 4615 | .mes_img_container:focus-within .mes_img_controls { |
| 4604 | 4616 | opacity: 1; |
| @@ -4612,6 +4624,17 @@ body:not(.caption) .mes_img_caption { | ||
| 4612 | 4624 | display: none; |
| 4613 | 4625 | } |
| 4614 | 4626 | |
| 4627 | +.mes_img_container:not(.img_swipes) .mes_img_swipes, | |
| 4628 | +body:not(.sd) .mes_img_swipes { | |
| 4629 | + display: none; | |
| 4630 | +} | |
| 4631 | + | |
| 4632 | +.mes_img_swipe_counter { | |
| 4633 | + font-weight: 600; | |
| 4634 | + filter: drop-shadow(2px 4px 6px black); | |
| 4635 | + cursor: default; | |
| 4636 | +} | |
| 4637 | + | |
| 4615 | 4638 | .img_enlarged_holder { |
| 4616 | 4639 | /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */ |
| 4617 | 4640 | min-height: 120px; |