Image Generation: Add swipes for generated images Supersedes #2648
| @@ -5801,6 +5801,11 @@ | |||
| 5801 | <div title="Caption" class="right_menu_button fa-lg fa-solid fa-envelope-open-text mes_img_caption" data-i18n="[title]Caption"></div> | 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 | <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div> | 5802 | <div title="Delete" class="right_menu_button fa-lg fa-solid fa-trash-can mes_img_delete" data-i18n="[title]Delete"></div> |
| 5803 | </div> | 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 | <img class="mes_img" src="" /> | 5809 | <img class="mes_img" src="" /> |
| 5805 | </div> | 5810 | </div> |
| 5806 | <div class="mes_bias"></div> | 5811 | <div class="mes_bias"></div> |
| @@ -459,6 +459,7 @@ export const event_types = { | |||
| 459 | LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register', | 459 | LLM_FUNCTION_TOOL_REGISTER: 'llm_function_tool_register', |
| 460 | LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', | 460 | LLM_FUNCTION_TOOL_CALL: 'llm_function_tool_call', |
| 461 | ONLINE_STATUS_CHANGED: 'online_status_changed', | 461 | ONLINE_STATUS_CHANGED: 'online_status_changed', |
| 462 | IMAGE_SWIPED: 'image_swiped', | ||
| 462 | }; | 463 | }; |
| 463 | 464 | ||
| 464 | export const eventSource = new EventEmitter(); | 465 | export const eventSource = new EventEmitter(); |
| @@ -2112,6 +2113,7 @@ export function updateMessageBlock(messageId, message) { | |||
| 2112 | export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | 2113 | export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { |
| 2113 | // Add image to message | 2114 | // Add image to message |
| 2114 | if (mes.extra?.image) { | 2115 | if (mes.extra?.image) { |
| 2116 | const container = messageElement.find('.mes_img_container'); | ||
| 2115 | const chatHeight = $('#chat').prop('scrollHeight'); | 2117 | const chatHeight = $('#chat').prop('scrollHeight'); |
| 2116 | const image = messageElement.find('.mes_img'); | 2118 | const image = messageElement.find('.mes_img'); |
| 2117 | const text = messageElement.find('.mes_text'); | 2119 | const text = messageElement.find('.mes_text'); |
| @@ -2127,9 +2129,27 @@ export function appendMediaToMessage(mes, messageElement, adjustScroll = true) { | |||
| 2127 | }); | 2129 | }); |
| 2128 | image.attr('src', mes.extra?.image); | 2130 | image.attr('src', mes.extra?.image); |
| 2129 | image.attr('title', mes.extra?.title || mes.title || ''); | 2131 | image.attr('title', mes.extra?.title || mes.title || ''); |
| 2130 | messageElement.find('.mes_img_container').addClass('img_extra'); | 2132 | container.addClass('img_extra'); |
| 2131 | image.toggleClass('img_inline', isInline); | 2133 | image.toggleClass('img_inline', isInline); |
| 2132 | text.toggleClass('displayNone', !isInline); | 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 | // Add file to message | 2155 | // Add file to message |
| @@ -59,6 +59,7 @@ const initiators = { | |||
| 59 | action: 'action', | 59 | action: 'action', |
| 60 | interactive: 'interactive', | 60 | interactive: 'interactive', |
| 61 | wand: 'wand', | 61 | wand: 'wand', |
| 62 | swipe: 'swipe', | ||
| 62 | }; | 63 | }; |
| 63 | 64 | ||
| 64 | const generationMode = { | 65 | const generationMode = { |
| @@ -3429,6 +3430,7 @@ async function sendMessage(prompt, image, generationType, additionalNegativePref | |||
| 3429 | generationType: generationType, | 3430 | generationType: generationType, |
| 3430 | negative: additionalNegativePrefix, | 3431 | negative: additionalNegativePrefix, |
| 3431 | inline_image: false, | 3432 | inline_image: false, |
| 3433 | image_swipes: [image], | ||
| 3432 | }, | 3434 | }, |
| 3433 | }; | 3435 | }; |
| 3434 | context.chat.push(message); | 3436 | context.chat.push(message); |
| @@ -3657,6 +3659,99 @@ async function writePromptFields(characterId) { | |||
| 3657 | await writeExtensionField(characterId, 'sd_character_prompt', promptObject); | 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 | jQuery(async () => { | 3755 | jQuery(async () => { |
| 3661 | await addSDGenButtons(); | 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 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); | 3895 | eventSource.on(event_types.CHAT_CHANGED, onChatChanged); |
| 3799 | 3896 | ||
| 3800 | await loadSettings(); | 3897 | await loadSettings(); |
| @@ -4569,6 +4569,7 @@ a { | |||
| 4569 | image-rendering: -webkit-optimize-contrast; | 4569 | image-rendering: -webkit-optimize-contrast; |
| 4570 | } | 4570 | } |
| 4571 | 4571 | ||
| 4572 | .mes_img_swipes, | ||
| 4572 | .mes_img_controls { | 4573 | .mes_img_controls { |
| 4573 | position: absolute; | 4574 | position: absolute; |
| 4574 | top: 0.1em; | 4575 | top: 0.1em; |
| @@ -4578,9 +4579,16 @@ a { | |||
| 4578 | opacity: 0; | 4579 | opacity: 0; |
| 4579 | flex-direction: row; | 4580 | flex-direction: row; |
| 4580 | justify-content: space-between; | 4581 | justify-content: space-between; |
| 4582 | align-items: center; | ||
| 4581 | padding: 1em; | 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 | .mes_img_controls .right_menu_button { | 4592 | .mes_img_controls .right_menu_button { |
| 4585 | filter: brightness(90%); | 4593 | filter: brightness(90%); |
| 4586 | text-shadow: 1px 1px var(--SmartThemeShadowColor) !important; | 4594 | text-shadow: 1px 1px var(--SmartThemeShadowColor) !important; |
| @@ -4589,16 +4597,20 @@ a { | |||
| 4589 | width: 1.25em; | 4597 | width: 1.25em; |
| 4590 | } | 4598 | } |
| 4591 | 4599 | ||
| 4600 | .mes_img_swipes .right_menu_button::before, | ||
| 4592 | .mes_img_controls .right_menu_button::before { | 4601 | .mes_img_controls .right_menu_button::before { |
| 4593 | /* Fix weird alignment with this font-awesome icons on focus */ | 4602 | /* Fix weird alignment with this font-awesome icons on focus */ |
| 4594 | position: relative; | 4603 | position: relative; |
| 4595 | top: 0.6125em; | 4604 | top: 0.6125em; |
| 4596 | } | 4605 | } |
| 4597 | 4606 | ||
| 4607 | .mes_img_swipes .right_menu_button:hover, | ||
| 4598 | .mes_img_controls .right_menu_button:hover { | 4608 | .mes_img_controls .right_menu_button:hover { |
| 4599 | filter: brightness(150%); | 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 | .mes_img_container:hover .mes_img_controls, | 4614 | .mes_img_container:hover .mes_img_controls, |
| 4603 | .mes_img_container:focus-within .mes_img_controls { | 4615 | .mes_img_container:focus-within .mes_img_controls { |
| 4604 | opacity: 1; | 4616 | opacity: 1; |
| @@ -4612,6 +4624,17 @@ body:not(.caption) .mes_img_caption { | |||
| 4612 | display: none; | 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 | .img_enlarged_holder { | 4638 | .img_enlarged_holder { |
| 4616 | /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */ | 4639 | /* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */ |
| 4617 | min-height: 120px; | 4640 | min-height: 120px; |