Backport `feat/chat-tree` and fix #4709 (#4712) * Performance improvements due to using chatElement instead of $('#chat'). * Reverts change `hideSwipeButtons()` change. https://github.com/SillyTavern/SillyTavern/pull/4576#discussion_r2394996102 * I’ve been working on a PR for issue #1731: [Swipes on every AI message, not just the last one](https://github.com/SillyTavern/SillyTavern/issues/1731). I’d appreciate feedback to avoid unnecessary effort. Currently, I have User and Assistant branches working. I have not touched `/api/chats/save` or '/api/chats/get', so branches do not persist. To keep `context.chat` unmodified (so extensions and the rest of the code remain unaffected), I’ve stored branches in `chatTree`: ```javascript chatTree = { branch_id: 1, branch: [ { mes: "Hi" }, { mes: "Hello", branch_id: 0, branch: [...] }, ] } ``` When a message is swiped, chatTree is updated via `saveChatToTree()`, then the next branch is loaded using get`ChatFromTree()`. Questions: According to Cohee: [This requires reorganizations in the file format for chats, not viable in the short term.](https://github.com/SillyTavern/SillyTavern/issues/1731#issuecomment-1937845036) I'm hesitant to proceed. May I store chatTree.json files alongside the existing .jsonl chats? Should I create a new /save endpoint, modify the existing one, or discontinue using .jsonl for saving. Known issues: Branches do not persist on refresh. Swiping multiple messages at once throws `Cannot read properties of undefined (reading 'mes') `. This will be fixed in the UI. Branches persist between chats/characters. This will be fixed when loading a chat. The Swipe arrows overlap with messages. No animation plays when swiping then editing a user message. * Fixed bug. Gemini: The calculation of mesId is fragile and contains a bug. The fallback chat[chat.length - 1] is a message object, and Number(object) will result in NaN. This will break the swipe functionality if the preceding expressions are falsy. Additionally, chat.indexOf(message) will almost always point to the last message in the chat, which is incorrect when swiping on an earlier message. A more robust approach is recommended to reliably get the message ID from the clicked element. const mesId = Number($(this).closest('.mes').attr('mesid')); * Fixes swipes on long chats. * chatTree is now persistent. Fixed bugs. * Fixed bugs. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2390042262 The logic to rename the chat tree file (.json) is currently in an unreachable else block. The if condition !fs.existsSync(pathToOriginalFile) || fs.existsSync(pathToRenamedFile) will almost always evaluate to true after the preceding copyFileSync and unlinkSync operations, because pathToOriginalFile will no longer exist. This prevents the chat tree file from being renamed, which breaks the persistence of message branches when a chat is renamed. The scrollChatToMessage function is implemented incorrectly. Calling .scrollTop() without any arguments on a jQuery object retrieves the current vertical scroll position; it does not scroll the element into view. This function will not have the intended effect of scrolling the chat to the specified message. * Fixed bugs. https://github.com/SillyTavern/SillyTavern/pull/4573#pullrequestreview-3282889120 There's a typo in the property name being deleted. It should be branch to match the data structure you've defined, not branches. This error will prevent the pruning logic from working correctly, potentially leading to corrupted or bloated chatTree data. The modified treeData is not being saved here. Instead, the global chatTree is being sent in the request. This will cause any renames of group members within message branches to be lost upon saving. You should send treeData, which contains the modifications. This function may have performance issues on large chats due to multiple structuredClone calls within a loop. swipelessMessage is created from a deep clone, and then it's deep-cloned again for every swipe. While this ensures data integrity, it is inefficient. Consider refactoring to reduce the number of deep-cloning operations, for instance, by creating swipelessMessage once per message and deep-cloning it only when creating a new branch for a swipe. * Fix warning. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2390183161 The logic for renaming the chat tree file seems to have a flaw. The condition !fs.existsSync(pathToOriginalTreeFile) || fs.existsSync(pathToRenamedTreeFile) will be true if the original tree file does not exist. In this case, a warning is logged. However, it's a valid scenario for a chat to not have a corresponding tree file, so no warning should be logged. The current logic could lead to confusing log messages. * Refactored chatTree into `public/scripts/chat-tree.js`. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2390269514 * Displaying swipes on past messages and the entire chat tree functionality is now an opt-in toggle. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2390273599 * Only allow one concurrent swipe. Fixed: Swiping multiple messages at once throws Cannot read properties of undefined (reading 'mes') . * Fixed bugs. Now `swipe_id >= swipes.length` is set to swipes.length. * Fixed bug. Swiping a user message did not re-show swipe buttons after the generation finished. * Moved `Show Swipes for All Messages` Toggle to `Chat/Message Handling`. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2395008961 * `JSON.stringify doesn't add spaces by default` https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2395047031 * Fixed "Tree file left behind when renaming chats" https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2395546142 * Fixed "Guard chatTree recursion in group member rename" https://github.com/SillyTavern/SillyTavern/pull/4573#pullrequestreview-3290587129 * Re-implement: https://github.com/SillyTavern/SillyTavern/pull/4576#discussion_r2395506501 * Moved chat trees to a separate directory. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2395045583 Enabled chat tree backups. * Removed structuredClone() for improved performance. * Fixed bugs. Characters are now correctly renamed in the chatTree. Directories are now recursively created. * Reverts `hideSwipeButtons` to original functionality when `show_swipes_for_all_messages` is false. https://github.com/SillyTavern/SillyTavern/pull/4576#discussion_r2395375718 * Added `refreshSwipeButtons`. Updated `showSwipeButtons` and `hideSwipeButtons`. Fixed bugs. * Fixed. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2400143193 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2400143202 * Added `clamp` to util.js. Refactored `swipe`. * Fixed bugs. https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2400685914 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2400685911 * Fixed bugs. * Fixed swipe animations. * Fixed bug by setting ids with `Number`. * Delete swipes, and bugfixes. * Merged `refactor/swipe`. * Fixed bug created in `Delete swipes, and bugfixes.`. * Merged from origin/staging. * Merged changes from refactor/swipe. * Fixed bug and refactored `syncWithSwipeId`. * Merged from `origin/staging`. * Fixed merge. * Fixed overlapping message generations. * Warn user, and refresh chat. * Added metadata to chatTree file. * Fixed "a sacrifice for the sake of simplicity." https://github.com/SillyTavern/SillyTavern/pull/2752#issuecomment-2323512022 Gemini: https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2464165990 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2464165991 * Improved swipeGenerate's animation. * Added `SWIPE_SOURCE` constant. * await `switchSwipesAllMessages`. * Added `swipeState`. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4573#issuecomment-3449690308 * Done: https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466884547 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466884547 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466894947 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466902075 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466911245 https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466938169 * Removed `NEUTRAL_CHAT_TREE_KEY` * Moved backups * Fixed `/send` and fixed a bug in `sendMessageAs`. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466913570 * Fixes usage of `ENOENT`: https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466929980 * Removed LLM attribution. * Updated link and warning. * Removed debugging comments. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4573#discussion_r2466905341 * Replaced `isSwipingAllowed` with `swipeState`. * Backported many changes from feat/chat-tree. * Refactored `showSwipeButtons` and `hideSwipeButtons` into `refreshSwipeButtons` * Refactored `showSwipeButtons` and `hideSwipeButtons` into `refreshSwipeButtons` * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2472109888 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2472113323 * Merged from `refactor/backport-chat-tree`. * Proposed fix for https://github.com/SillyTavern/SillyTavern/issues/4709 Reverts commit: https://github.com/SillyTavern/SillyTavern/commit/1b3db273891c1ba8c781d2f691ad2e41937ca2aa On PR: https://github.com/SillyTavern/SillyTavern/pull/2940 * Proposed fix for https://github.com/SillyTavern/SillyTavern/issues/4709 Reverts commit: https://github.com/SillyTavern/SillyTavern/commit/1b3db273891c1ba8c781d2f691ad2e41937ca2aa On PR: https://github.com/SillyTavern/SillyTavern/pull/2940 * Fixed regenerate and continue while editing the last message. * Added swipesHidden. Messages can now be designated as non-swipeable with `message.extra.swipeable`. * Use `.hidden` and classes instead of `.css` to display swipe chevrons. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3465749700 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2476136375 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2476140043 https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3465776056 https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3465794330 * Fixed bug. * Fixed broken JQuery animation. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2479003956 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2479011525 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2479014001 * Fixed active arrow transition. * Fixed swipe shake direction. * Fixed truthy check. * Added temporary link to documentation PR. * Moved `switchSwipesAllMessages` * Fixed failed generations deleting the branch. Fixed `forceSwipeId`. Always call `saveChatConditional` if the `swipe_id` has changed. * ESLint. * Fixed bug. Better `syncSwipeToMes` error handling. * Backported changes from `feat/chat-tree` Added failed swipe animation. Fixed `newSwipeId`. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#pullrequestreview-3406878337 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2483143387 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2483143387 * Added `showSwipes: false` to `showMoreMessages`. And a few more performance improvements. * Fixed animations. * Added `'.mes'` to `.children`. * Significantly improved `animateSwipeTransition` performance on large chats. * Improved `syncSwipeToMes` error handling. * Significantly improved `animateSwipeTransition` performance on large chats. * Improved `syncSwipeToMes` error handling. * Corrected Merge. * Corrected Merge. * Improved: `redisplayChat` https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2484177521 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2484215469 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2484217230 * Added a temporary implementation of `branchChat`. * Fixed bug when generating a swipe. * Fixed bug in `branchChat`. * Fixed Off by one error due to `chatElement.children()` selecting `show_more_messages`. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3478331073 * Fixed Off by one error due to `chatElement.children()` selecting `show_more_messages`. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3478331073 * Cleaned system messages array and moved `swipeable` * Removed CSS nesting. * Fixed merge. * Improved `syncSwipeToMes` error handling. * Matched `swipes-counter` fade to chevrons. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3478192184 * Wrapped `transition-behavior: allow-discrete;` in `@supports`. * Fixed types. * Improved swipeability feedback and functions. * Improved `refreshSwipeButtons` performance again. * Swapped `.attr` to `.prop`. * Fixed: `@supports (transition-behavior: allow-discrete)` * Fixed and clarified typo. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495919425 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495923036 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495927019 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495930080 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495943870 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495950927 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495954387 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495974324 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495974683 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495957181 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495960920 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495968137 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495968922 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495959430 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495924599 * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495918468 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495931979 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495933092 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2495939511 * Improved `refreshSwipeButtons` performance by skipping 'swipes-counter' updates by default. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2496382916 https://github.com/SillyTavern/SillyTavern/pull/4712#discussion_r2496379401 * Removed chevron fade-in on Cohee's request: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3493829978 * Set most system messages to `swipeable: false`. * Implemented `OVERSWIPE_BEHAVIOR`. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3499706714 * Fix formatting, make comments IDE friendly * IDE friendly enum comments * Move animation frames to a dedicated file * Do not compare with -1 * overswipeBehavior => getOverswipeBehavior * Formatting fix * Don't let regenerate on is_system * Fixed canceled generations in pristine chats. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3499819670 * Fixed `AUTO_SWIPE` when `animation_duration = 0`. * Fixed mistake in `redisplayChat`. * Holding the swipe button speeds up `swipeDuration`. * Fixed 'animationend' never ending. Altered resetTime. * Swapped from `saveChatConditional` to `saveChatDebounced` in `swipe`. * Skip the animation if it's faster than 50ms instead of 10ms. * Typo. * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#pullrequestreview-3457198109 * Adjust duration reset cooldown * Add quotes to selector * Specify type for message parameter in swipe function * Add type for swipe UI event * Disabled fade-in during printMessages. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3539315919 * Typo. * Added quotes to selector . * Reduce reset time 500 -> 350 * Loops do not cause a generation so their chevrons should not have increased opacity. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3543692019 * Revert reset time * Renamed `heldSwipes` to `recentSwipes`. * Autofix the swipes array during `updateSwipeCounter`. * User messages should not have swipes. * Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373 * Improve formatting * Fixed: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3559617088 * Show `pristineGreetingSwipeNotice` once. * `clearMessageData` when swipe-regenerating a message. * accountStorage is already imported in the module * Removed `await`. * Removed pristine greeting notice. https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3560758598 * Removed redundant functions in `StreamingProcessor` and fixed streamed replies missing counters. * Moved `markUIGenStopped` after `eventSource.emit`. Swapped to `saveChatDebounced` to fix: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3567014810. * Save a structuredClone of `chat` to prevent an invalid chat from being saved. * Only `structuredClone` `chat` on `saveChatDebounced`. * Revert "Only `structuredClone` `chat` on `saveChatDebounced`." This reverts commit 49498b7aa1410107b294555fb945d977e60bfebf. * Revert "Save a structuredClone of `chat` to prevent an invalid chat from being saved." This reverts commit 5f137ed1380107fde0765b951dc634081bdbf2ff. * Prevent `saveChatDebounced` from saving while the swipe is in progress. See: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3567077312 * Fixed animation never ending: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3567106213 * `forceMesId` and `forceSwipeId` are not objects. * Fixed Reduced Motion causing a warning when swiping back. * Only hide `.mes_buttons` when generating. * Fix eslint * Reset duration on switching direction --------- Co-authored-by: user <user@exmaple.com> Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>
Signed| @@ -132,3 +132,23 @@ | ||
| 132 | 132 | overflow-y: hidden; |
| 133 | 133 | } |
| 134 | 134 | } |
| 135 | + | |
| 136 | +@keyframes slide { | |
| 137 | + 0% { | |
| 138 | + transform: translateX(var(--slide-mes-x-start, 0px)); | |
| 139 | + } | |
| 140 | + | |
| 141 | + 100% { | |
| 142 | + transform: translateX(var(--slide-mes-x-end, 0px)); | |
| 143 | + } | |
| 144 | +} | |
| 145 | + | |
| 146 | +@-webkit-keyframes slide { | |
| 147 | + 0% { | |
| 148 | + transform: translateX(var(--slide-mes-x-start, 0px)); | |
| 149 | + } | |
| 150 | + | |
| 151 | + 100% { | |
| 152 | + transform: translateX(var(--slide-mes-x-end, 0px)); | |
| 153 | + } | |
| 154 | +} | |
| @@ -6,6 +6,7 @@ import { oai_settings } from './scripts/openai'; | ||
| 6 | 6 | import { textgenerationwebui_settings } from './scripts/textgen-settings'; |
| 7 | 7 | import { FileAttachment } from './scripts/chats'; |
| 8 | 8 | import { ReasoningMessageExtra } from './scripts/reasoning'; |
| 9 | +import { OVERSWIPE_BEHAVIOR } from './scripts/constants'; | |
| 9 | 10 | |
| 10 | 11 | declare global { |
| 11 | 12 | // Custom types |
| @@ -80,6 +81,9 @@ declare global { | ||
| 80 | 81 | title?: string; |
| 81 | 82 | isSmallSys?: boolean; |
| 82 | 83 | token_count?: number; |
| 84 | + /** When false, the message cannot be swiped. */ | |
| 85 | + swipeable?: boolean; | |
| 86 | + overswipe_behavior?: OVERSWIPE_BEHAVIOR; | |
| 83 | 87 | files?: FileAttachment[]; |
| 84 | 88 | inline_image?: boolean; |
| 85 | 89 | media_display?: string; |
| @@ -202,4 +206,6 @@ declare global { | ||
| 202 | 206 | rgba: string; |
| 203 | 207 | } |
| 204 | 208 | }; |
| 209 | + | |
| 210 | + type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>; | |
| 205 | 211 | } |
| @@ -7052,7 +7052,7 @@ | ||
| 7052 | 7052 | <div class="mes_timer"></div> |
| 7053 | 7053 | <div class="tokenCounterDisplay"></div> |
| 7054 | 7054 | </div> |
| 7055 | 7055 | <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div> |
| 7056 | 7056 | <div class="mes_block"> |
| 7057 | 7057 | <div class="ch_name flex-container justifySpaceBetween"> |
| 7058 | 7058 | <div class="flex-container flex1 alignitemscenter"> |
| @@ -7116,7 +7116,7 @@ | ||
| 7116 | 7116 | <div class="mes_bias"></div> |
| 7117 | 7117 | </div> |
| 7118 | 7118 | <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap"> |
| 7119 | 7119 | <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div> |
| 7120 | 7120 | <div class="swipes-counter"></div> |
| 7121 | 7121 | </div> |
| 7122 | 7122 | </div> |
| @@ -181,8 +181,10 @@ import { | ||
| 181 | 181 | canUseNegativeLookbehind, |
| 182 | 182 | trimSpaces, |
| 183 | 183 | clamp, |
| 184 | + shakeElement, | |
| 185 | + createTimeout, | |
| 184 | 186 | } from './scripts/utils.js'; |
| 185 | 187 | import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, OVERSWIPE_BEHAVIOR, SCROLL_BEHAVIOR, SWIPE_DIRECTION, SWIPE_SOURCE, SWIPE_STATE } from './scripts/constants.js'; |
| 186 | 188 | |
| 187 | 189 | import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js'; |
| 188 | 190 | import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js'; |
| @@ -372,7 +374,11 @@ export let name1 = default_user_name; | ||
| 372 | 374 | export let name2 = systemUserName; |
| 373 | 375 | /** @type {ChatMessage[]} */ |
| 374 | 376 | export let chat = []; |
| 375 | -export let isSwipingAllowed = true; //false when a swipe is in progress, or swiping is blocked. | |
| 377 | + | |
| 378 | +/** | |
| 379 | + * @type {import('./scripts/constants.js').SWIPE_STATE} | |
| 380 | + */ | |
| 381 | +export let swipeState = SWIPE_STATE.NONE; | |
| 376 | 382 | let chatSaveTimeout; |
| 377 | 383 | let importFlashTimeout; |
| 378 | 384 | export let isChatSaving = false; |
| @@ -561,6 +567,7 @@ let chat_file_for_del = ''; | ||
| 561 | 567 | export let online_status = 'no_connection'; |
| 562 | 568 | |
| 563 | 569 | export let is_send_press = false; //Send generation |
| 570 | +export const isGenerating = () => (is_send_press || is_group_generating); | |
| 564 | 571 | |
| 565 | 572 | let this_del_mes = -1; |
| 566 | 573 | |
| @@ -574,7 +581,14 @@ export let settings; | ||
| 574 | 581 | export let amount_gen = 80; //default max length of AI generated responses |
| 575 | 582 | export let max_context = 2048; |
| 576 | 583 | |
| 577 | -var swipes = true; | |
| 584 | +/** User preference for swipeable messages */ | |
| 585 | +let swipes = true; | |
| 586 | +/** Forcefully hide swipes. */ | |
| 587 | +export let swipesHidden = false; | |
| 588 | +/** @type {{ now: number, direction: string }} */ | |
| 589 | +export let lastSwipeInfo = { now: performance.now(), direction: SWIPE_DIRECTION.RIGHT }; | |
| 590 | +export let recentSwipes = 0; | |
| 591 | + | |
| 578 | 592 | export let extension_prompts = {}; |
| 579 | 593 | |
| 580 | 594 | export let main_api;// = "kobold"; |
| @@ -1376,10 +1390,11 @@ export async function showMoreMessages(messagesToLoad = null) { | ||
| 1376 | 1390 | |
| 1377 | 1391 | while (messageId > 0 && count > 0) { |
| 1378 | 1392 | let newMessageId = messageId - 1; |
| 1379 | 1393 | addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false }); |
| 1380 | 1394 | count--; |
| 1381 | 1395 | messageId--; |
| 1382 | 1396 | } |
| 1397 | + refreshSwipeButtons(); | |
| 1383 | 1398 | |
| 1384 | 1399 | if (messageId == 0) { |
| 1385 | 1400 | $('#show_more_messages').remove(); |
| @@ -1410,7 +1425,7 @@ export async function printMessages() { | ||
| 1410 | 1425 | |
| 1411 | 1426 | chatElement.find('.mes').removeClass('last_mes'); |
| 1412 | 1427 | chatElement.find('.mes').last().addClass('last_mes'); |
| 1413 | 1428 | refreshSwipeButtons(false, false); |
| 1414 | 1429 | applyStylePins(); |
| 1415 | 1430 | scrollChatToBottom({ waitForFrame: true }); |
| 1416 | 1431 | delay(debounce_timeout.short).then(() => scrollOnMediaLoad()); |
| @@ -1471,6 +1486,7 @@ export async function clearChat() { | ||
| 1471 | 1486 | if (is_delete_mode) { |
| 1472 | 1487 | $('#dialogue_del_mes_cancel').trigger('click'); |
| 1473 | 1488 | } |
| 1489 | + //This will also remove non '.mes' elements, e.g. '<div id="show_more_messages">Show more messages</div>'. | |
| 1474 | 1490 | chatElement.children().remove(); |
| 1475 | 1491 | if ($('.zoomed_avatar[forChar]').length) { |
| 1476 | 1492 | console.debug('saw avatars to remove'); |
| @@ -1575,9 +1591,17 @@ export async function reloadCurrentChat() { | ||
| 1575 | 1591 | * Send the message currently typed into the chat box. |
| 1576 | 1592 | */ |
| 1577 | 1593 | export async function sendTextareaMessage() { |
| 1594 | + // don't proceed during swipeGenerate() | |
| 1595 | + if (swipeState == SWIPE_STATE.EDITING) { | |
| 1596 | + toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`); | |
| 1597 | + return; | |
| 1598 | + } | |
| 1599 | + if (swipeState !== SWIPE_STATE.NONE) return; // don't proceed if mid-swipe. | |
| 1578 | 1600 | if (is_send_press) return; |
| 1579 | 1601 | if (isExecutingCommandsFromChatInput) return; |
| 1580 | 1602 | |
| 1603 | + hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible. | |
| 1604 | + | |
| 1581 | 1605 | let generateType = 'normal'; |
| 1582 | 1606 | // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last |
| 1583 | 1607 | // message was sent from a character (not the user or the system). |
| @@ -1597,7 +1621,9 @@ export async function sendTextareaMessage() { | ||
| 1597 | 1621 | await newAssistantChat({ temporary: false }); |
| 1598 | 1622 | } |
| 1599 | 1623 | |
| 1600 | 1624 | returnlet generation = await Generate(generateType); |
| 1625 | + showSwipeButtons(); | |
| 1626 | + return generation; | |
| 1601 | 1627 | } |
| 1602 | 1628 | |
| 1603 | 1629 | /** |
| @@ -2362,7 +2388,7 @@ export function addCopyToCodeBlocks(messageElement) { | ||
| 2362 | 2388 | * @param {boolean} [options.scroll=true] Whether to scroll to the new message |
| 2363 | 2389 | * @param {number} [options.insertBefore=null] Message ID to insert the new message before |
| 2364 | 2390 | * @param {number} [options.forceId=null] Force the message ID |
| 2365 | 2391 | * @param {boolean} [options.showSwipes=true] Whether to showrefresh the swipe buttons. |
| 2366 | 2392 | * @returns {void} |
| 2367 | 2393 | */ |
| 2368 | 2394 | export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) { |
| @@ -2486,8 +2512,7 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2486 | 2512 | }); |
| 2487 | 2513 | |
| 2488 | 2514 | if (type === 'swipe') { |
| 2489 | - const messageId = forceId ?? chat.length - 1; | |
| 2515 | + const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`); | |
| 2490 | - const swipeMessage = chatElement.find(`[mesid="${messageId}"]`); | |
| 2491 | 2516 | swipeMessage.attr('swipeid', params.swipeId); |
| 2492 | 2517 | swipeMessage.find('.mes_text').html(messageText).attr('title', title); |
| 2493 | 2518 | swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`); |
| @@ -2505,24 +2530,21 @@ export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll | ||
| 2505 | 2530 | swipeMessage.find('.tokenCounterDisplay').empty(); |
| 2506 | 2531 | } |
| 2507 | 2532 | } else { |
| 2508 | - const messageId = forceId ?? chat.length - 1; | |
| 2533 | + chatElement.find(`[mesid="${newMessageId}"] .mes_text`).append(messageText); | |
| 2509 | - chatElement.find(`[mesid="${messageId}"] .mes_text`).append(messageText); | |
| 2510 | 2534 | appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE); |
| 2511 | - showSwipes && hideSwipeButtons(); | |
| 2512 | 2535 | } |
| 2513 | 2536 | |
| 2514 | 2537 | addCopyToCodeBlocks(newMessage); |
| 2515 | 2538 | |
| 2516 | 2539 | // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message'all isnon-user enabledmessages. |
| 2517 | - if (!params.isUser && newMessageId !== 0 && newMessageId !== chat.length - 1) { | |
| 2540 | + if (!params.isUser) { | |
| 2518 | - const swipesNum = chat[newMessageId].swipes?.length; | |
| 2541 | + updateSwipeCounter(newMessageId); | |
| 2519 | - const swipeId = chat[newMessageId].swipe_id + 1; | |
| 2520 | - newMessage.find('.swipes-counter').text(formatSwipeCounter(swipeId, swipesNum)); | |
| 2521 | 2542 | } |
| 2522 | 2543 | |
| 2523 | - if (showSwipes) { | |
| 2544 | + //last_mes should always be updated. | |
| 2545 | + chatElement.find('.mes').removeClass('last_mes'); | |
| 2524 | 2546 | chatElement.find('.mes').last().addClass('last_mes'); |
| 2525 | - chatElement.find('.mes').eq(-2).removeClass('last_mes'); | |
| 2547 | + if (showSwipes) { | |
| 2526 | 2548 | refreshSwipeButtons(); |
| 2527 | 2549 | } |
| 2528 | 2550 | |
| @@ -3275,7 +3297,7 @@ class StreamingProcessor { | ||
| 3275 | 3297 | } |
| 3276 | 3298 | |
| 3277 | 3299 | markUIGenStopped() { |
| 3278 | 3300 | activateSendButtonsunblockGeneration(); |
| 3279 | 3301 | } |
| 3280 | 3302 | |
| 3281 | 3303 | async onStartStreaming(text) { |
| @@ -3404,14 +3426,14 @@ class StreamingProcessor { | ||
| 3404 | 3426 | } |
| 3405 | 3427 | |
| 3406 | 3428 | async onFinishStreaming(messageId, text) { |
| 3407 | - this.markUIGenStopped(); | |
| 3408 | 3429 | await this.onProgressStreaming(messageId, text, true); |
| 3409 | 3430 | addCopyToCodeBlocks(const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`)); |
| 3431 | + const message = chat[messageId]; | |
| 3432 | + addCopyToCodeBlocks(messageElement); | |
| 3410 | 3433 | |
| 3411 | 3434 | await this.reasoningHandler.finish(messageId); |
| 3412 | 3435 | |
| 3413 | 3436 | if (Array.isArray(this.swipes) && this.swipes.length > 0) { |
| 3414 | - const message = chat[messageId]; | |
| 3415 | 3437 | const swipeInfoExtra = structuredClone(message.extra ?? {}); |
| 3416 | 3438 | delete swipeInfoExtra.token_count; |
| 3417 | 3439 | delete swipeInfoExtra.reasoning; |
| @@ -3424,15 +3446,20 @@ class StreamingProcessor { | ||
| 3424 | 3446 | }; |
| 3425 | 3447 | const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo)); |
| 3426 | 3448 | parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration); |
| 3427 | 3449 | chat[messageId]message.swipes.push(...this.swipes); |
| 3428 | 3450 | chat[messageId]message.swipe_info.push(...swipeInfoArray); |
| 3429 | 3451 | } |
| 3430 | 3452 | |
| 3453 | + syncMesToSwipe(messageId); | |
| 3454 | + saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); | |
| 3455 | + | |
| 3431 | 3456 | if (Array.isArray(this.images) && this.images.length > 0) { |
| 3432 | 3457 | await processImageAttachment(chat[messageId]message, { imageUrls: this.images }); |
| 3433 | 3458 | appendMediaToMessage(chat[messageId]message, $(this.messageDom)); |
| 3434 | 3459 | } |
| 3435 | 3460 | |
| 3461 | + this.markUIGenStopped(); | |
| 3462 | + | |
| 3436 | 3463 | if (this.type !== 'impersonate') { |
| 3437 | 3464 | await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type); |
| 3438 | 3465 | await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type); |
| @@ -3440,15 +3467,13 @@ class StreamingProcessor { | ||
| 3440 | 3467 | await eventSource.emit(event_types.IMPERSONATE_READY, text); |
| 3441 | 3468 | } |
| 3442 | 3469 | |
| 3443 | - syncMesToSwipe(messageId); | |
| 3470 | + updateSwipeCounter(messageId, { message, messageElement }); | |
| 3444 | - saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage); | |
| 3445 | - await saveChatConditional(); | |
| 3446 | - unblockGeneration(); | |
| 3447 | 3471 | |
| 3448 | 3472 | const isAborted = this.abortController.signal.aborted; |
| 3449 | 3473 | if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) { |
| 3450 | - return swipe_right(); | |
| 3474 | + return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 }); | |
| 3451 | 3475 | } |
| 3476 | + saveChatDebounced(); | |
| 3452 | 3477 | |
| 3453 | 3478 | playMessageSound(); |
| 3454 | 3479 | } |
| @@ -3458,7 +3483,6 @@ class StreamingProcessor { | ||
| 3458 | 3483 | this.isStopped = true; |
| 3459 | 3484 | |
| 3460 | 3485 | this.markUIGenStopped(); |
| 3461 | - unblockGeneration(); | |
| 3462 | 3486 | |
| 3463 | 3487 | const noEmitTypes = ['swipe', 'impersonate', 'continue']; |
| 3464 | 3488 | if (!noEmitTypes.includes(this.type)) { |
| @@ -5171,7 +5195,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro | ||
| 5171 | 5195 | const isAborted = abortController && abortController.signal.aborted; |
| 5172 | 5196 | if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) { |
| 5173 | 5197 | is_send_press = false; |
| 5174 | - return swipe_right(); | |
| 5198 | + return await swipe(null, SWIPE_DIRECTION.RIGHT, { source: SWIPE_SOURCE.AUTO_SWIPE, repeated: true, forceMesId: chat.length - 1 }); | |
| 5199 | + | |
| 5175 | 5200 | } |
| 5176 | 5201 | |
| 5177 | 5202 | console.debug('/api/chats/save called by /Generate'); |
| @@ -5303,7 +5328,6 @@ function unblockGeneration(type) { | ||
| 5303 | 5328 | |
| 5304 | 5329 | is_send_press = false; |
| 5305 | 5330 | activateSendButtons(); |
| 5306 | - showSwipeButtons(); | |
| 5307 | 5331 | setGenerationProgress(0); |
| 5308 | 5332 | flushEphemeralStoppingStrings(); |
| 5309 | 5333 | flushWIInjections(); |
| @@ -6358,6 +6382,47 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title | ||
| 6358 | 6382 | } |
| 6359 | 6383 | |
| 6360 | 6384 | /** |
| 6385 | + * Creates a message's `swipes`, `swipe_id` and `swipe_info` if necessary. | |
| 6386 | + * @param {ChatMessage} message | |
| 6387 | + * @returns {boolean} true if the message was updated. | |
| 6388 | + */ | |
| 6389 | +export function ensureSwipes(message) { | |
| 6390 | + let updated = false; | |
| 6391 | + | |
| 6392 | + if (!message || typeof message !== 'object') { | |
| 6393 | + console.trace(`[ensureSwipes] failed. '${message}' is not an object.`); | |
| 6394 | + return updated; | |
| 6395 | + } | |
| 6396 | + | |
| 6397 | + //Small system messages and user messages should not have swipes. | |
| 6398 | + if (message?.is_user || message?.extra?.isSmallSys) { | |
| 6399 | + return updated; | |
| 6400 | + } | |
| 6401 | + | |
| 6402 | + if (!Array.isArray(message.swipes)) { | |
| 6403 | + message.swipes = [message.mes ?? '']; | |
| 6404 | + updated = true; | |
| 6405 | + } | |
| 6406 | + | |
| 6407 | + if (typeof message.swipe_id !== 'number') { | |
| 6408 | + message.swipe_id = 0; | |
| 6409 | + updated = true; | |
| 6410 | + } | |
| 6411 | + | |
| 6412 | + if (!Array.isArray(message.swipe_info)) { | |
| 6413 | + message.swipe_info = message.swipes.map(_ => ({ | |
| 6414 | + send_date: message.send_date, | |
| 6415 | + gen_started: message.gen_started, | |
| 6416 | + gen_finished: message.gen_finished, | |
| 6417 | + extra: structuredClone(message.extra) ?? {}, | |
| 6418 | + })); | |
| 6419 | + updated = true; | |
| 6420 | + } | |
| 6421 | + | |
| 6422 | + return updated; | |
| 6423 | +} | |
| 6424 | + | |
| 6425 | +/** | |
| 6361 | 6426 | * Syncs the current message and all its data into the swipe data at the given message ID (or the last message if no ID is given). |
| 6362 | 6427 | * |
| 6363 | 6428 | * If the swipe data is invalid in some way, this function will exit out without doing anything. |
| @@ -6539,6 +6604,7 @@ function getGeneratingModel(mes) { | ||
| 6539 | 6604 | export function activateSendButtons() { |
| 6540 | 6605 | is_send_press = false; |
| 6541 | 6606 | hideStopButton(); |
| 6607 | + showSwipeButtons(); | |
| 6542 | 6608 | delete document.body.dataset.generating; |
| 6543 | 6609 | } |
| 6544 | 6610 | |
| @@ -6547,6 +6613,7 @@ export function activateSendButtons() { | ||
| 6547 | 6613 | */ |
| 6548 | 6614 | export function deactivateSendButtons() { |
| 6549 | 6615 | showStopButton(); |
| 6616 | + hideSwipeButtons(); | |
| 6550 | 6617 | document.body.dataset.generating = 'true'; |
| 6551 | 6618 | } |
| 6552 | 6619 | |
| @@ -7583,8 +7650,11 @@ function updateMessage(div) { | ||
| 7583 | 7650 | const mesElement = div.closest('.mes'); |
| 7584 | 7651 | const mes = chat[mesElement.attr('mesid')]; |
| 7585 | 7652 | |
| 7653 | + // editing old messages | |
| 7654 | + mes['extra'] ??= {}; | |
| 7655 | + | |
| 7586 | 7656 | let regexPlacement; |
| 7587 | 7657 | if (mes?.is_user) { |
| 7588 | 7658 | regexPlacement = regex_placement.USER_INPUT; |
| 7589 | 7659 | } else if (mes.extra?.type === 'narrator') { |
| 7590 | 7660 | regexPlacement = regex_placement.SLASH_COMMAND; |
| @@ -7614,15 +7684,11 @@ function updateMessage(div) { | ||
| 7614 | 7684 | } |
| 7615 | 7685 | mes['mes'] = text; |
| 7616 | 7686 | if (mes['swipe_id'] !== undefined) { |
| 7687 | + ensureSwipes(mes); | |
| 7617 | 7688 | mes['swipes'][mes['swipe_id']] = text; |
| 7618 | 7689 | } |
| 7619 | 7690 | |
| 7620 | - // editing old messages | |
| 7691 | + if (mes?.is_system || mes?.is_user || mes.extra?.type === system_message_types.NARRATOR) { | |
| 7621 | - if (!mes.extra) { | |
| 7622 | - mes.extra = {}; | |
| 7623 | - } | |
| 7624 | - | |
| 7625 | - if (mes.is_system || mes.is_user || mes.extra.type === system_message_types.NARRATOR) { | |
| 7626 | 7692 | mes.extra.bias = bias ?? null; |
| 7627 | 7693 | } else { |
| 7628 | 7694 | mes.extra.bias = null; |
| @@ -7693,8 +7759,7 @@ export async function messageEdit(editMessageId) { | ||
| 7693 | 7759 | this_edit_mes_id = editMessageId; |
| 7694 | 7760 | this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2); |
| 7695 | 7761 | |
| 7696 | - const hideCounters = editMessageId < chat.length - 1; | |
| 7762 | + refreshSwipeButtons(); | |
| 7697 | - hideSwipeButtons({ hideCounters }); | |
| 7698 | 7763 | |
| 7699 | 7764 | const chatScrollPosition = chatElement.scrollTop(); |
| 7700 | 7765 | const messageBlock = messageElement.find('.mes_block'); |
| @@ -7749,7 +7814,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) { | ||
| 7749 | 7814 | if (this?.classList?.contains('mes_edit_cancel')) { |
| 7750 | 7815 | thisMesDiv = $(this).closest('.mes'); |
| 7751 | 7816 | } else { |
| 7752 | 7817 | thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`); |
| 7753 | 7818 | } |
| 7754 | 7819 | |
| 7755 | 7820 | const thisMesBlock = thisMesDiv.find('.mes_block'); |
| @@ -7830,11 +7895,17 @@ async function messageEditMove(sourceId, targetId) { | ||
| 7830 | 7895 | } |
| 7831 | 7896 | |
| 7832 | 7897 | updateViewMessageIds(); |
| 7898 | + refreshSwipeButtons(); | |
| 7833 | 7899 | await saveChatConditional(); |
| 7834 | 7900 | return true; |
| 7835 | 7901 | } |
| 7836 | 7902 | |
| 7837 | 7903 | async function messageEditDone(div) { |
| 7904 | + if (!(this_edit_mes_id >= 0)) { | |
| 7905 | + console.trace('this_edit_mes_id cannot be blank when calling messageEditDone.'); | |
| 7906 | + return; | |
| 7907 | + } | |
| 7908 | + | |
| 7838 | 7909 | let { mesBlock, text, mes, bias } = updateMessage(div); |
| 7839 | 7910 | if (this_edit_mes_id == 0) { |
| 7840 | 7911 | text = substituteParams(text); |
| @@ -8559,99 +8630,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w | ||
| 8559 | 8630 | |
| 8560 | 8631 | /** |
| 8561 | 8632 | * Update the swipe counter for mesId. |
| 8633 | + * By default, the swipe counter's opacity will appear greyed out. The opacity is changed with CSS. | |
| 8562 | 8634 | * @param {Number} mesId |
| 8635 | + * @param {object} [options] Options | |
| 8636 | + * @param {ChatMessage} [options.message=undefined] Swipe numbers from this message will be used instead of mesId. | |
| 8637 | + * @param {JQuery<HTMLElement>} [options.messageElement=undefined] Target Element. Passing in the message's element will save a DOM query. | |
| 8563 | 8638 | */ |
| 8564 | 8639 | export async function updateSwipeCounter(mesId, { message = undefined, messageElement = undefined } = {}) { |
| 8565 | - const swipeCounterText = formatSwipeCounter((chat[mesId]?.['swipe_id'] + 1), chat[mesId]?.['swipes']?.length); | |
| 8640 | + message ??= chat[mesId]; | |
| 8566 | 8641 | const currentMessagemessageElement ??= chatElement.children('.mes').filter(`[mesid="${mesId}"]`); |
| 8567 | - const swipeCounter = currentMessage.find('.swipes-counter'); | |
| 8642 | + | |
| 8568 | - swipeCounter.text(swipeCounterText).show(); | |
| 8643 | + //If the message does not have swipes, create them. | |
| 8644 | + if (ensureSwipes(message)) { | |
| 8645 | + syncMesToSwipe(mesId); | |
| 8646 | + } | |
| 8647 | + | |
| 8648 | + const swipeCounterText = formatSwipeCounter((message?.swipe_id + 1), message?.swipes?.length); | |
| 8649 | + const swipeCounter = messageElement.find('.swipes-counter'); | |
| 8650 | + swipeCounter.text(swipeCounterText).prop('hidden', false); | |
| 8569 | 8651 | } |
| 8570 | 8652 | |
| 8571 | 8653 | /** |
| 8572 | 8654 | * Swipe buttons areReturns oftentrue toggledif tomessages updateare theirgenerally positionswipeable. |
| 8573 | - * This should be replaced with a more efficient function. | |
| 8655 | + * @returns {boolean} | |
| 8574 | 8656 | */ |
| 8575 | 8657 | export function refreshSwipeButtonsisSwipingAllowed() { |
| 8576 | - hideSwipeButtons(); | |
| 8658 | + return ( | |
| 8577 | - showSwipeButtons(); | |
| 8659 | + //Swipe cannot be called on an empty chat. | |
| 8660 | + chat.length !== 0 && | |
| 8661 | + //The swipes setting must be enabled, and swipes can't be hidden. | |
| 8662 | + swipes && !swipesHidden && | |
| 8663 | + //Cannot swipe while generating. | |
| 8664 | + !isGenerating() && | |
| 8665 | + //If mid-swipe, the message cannot be swiped. | |
| 8666 | + swipeState === SWIPE_STATE.NONE | |
| 8667 | + ); | |
| 8578 | 8668 | } |
| 8579 | 8669 | |
| 8580 | -export function showSwipeButtons(mesId = chat.length - 1) { | |
| 8670 | +/** | |
| 8581 | - isSwipingAllowed = true; | |
| 8671 | + * Returns true if the message is swipeable. | |
| 8672 | + * This does not check if messages are generally swipeable. See isSwipingAllowed(). | |
| 8673 | + * This does not check if the swipes exist or are valid. | |
| 8674 | + * @param {number} messageId The message Id to check. | |
| 8675 | + * @param {ChatMessage} [message=undefined] If undefined, then the message checks will be skipped. | |
| 8676 | + * @returns {boolean} | |
| 8677 | + */ | |
| 8678 | +export function isMessageSwipeable(messageId, message = undefined) { | |
| 8679 | + message ??= chat[messageId]; | |
| 8582 | 8680 | |
| 8583 | - if (chat.length === 0) { | |
| 8681 | + //If the message does not have swipes, create them. | |
| 8584 | - return; | |
| 8682 | + if (ensureSwipes(message)) { | |
| 8683 | + syncMesToSwipe(messageId); | |
| 8585 | 8684 | } |
| 8586 | 8685 | |
| 8587 | 8686 | if ( |
| 8588 | - chat[mesId].is_system || | |
| 8687 | + //Only messages below the currently edited message can be swiped, if it's not mid-swipe edit. | |
| 8589 | - !swipes || | |
| 8688 | + ((messageId > (this_edit_mes_id ?? -1)) && (swipeState != SWIPE_STATE.EDITING)) && | |
| 8590 | - Number($('.mes:last').attr('mesid')) < 0 || | |
| 8689 | + | |
| 8591 | - chat[mesId].is_user || | |
| 8690 | + //If the message is the last message, and it exists. | |
| 8592 | - (selected_group && is_group_generating) | |
| 8691 | + (messageId == chat.length - 1) && | |
| 8593 | - ) { | |
| 8692 | + (message && | |
| 8594 | - return; | |
| 8693 | + //Small system messages cannot be swiped. | |
| 8694 | + !(message?.extra?.isSmallSys) && | |
| 8695 | + //Some messages, like the welcome screen, are not swipeable. | |
| 8696 | + !(message?.extra?.swipeable === false) && | |
| 8697 | + //User messages are not swipeable. | |
| 8698 | + !message.is_user | |
| 8699 | + ) | |
| 8700 | + ) | |
| 8701 | + //The message is swipeable. | |
| 8702 | + { return true; } | |
| 8703 | + //The message is not swipeable. | |
| 8704 | + else { return false; } | |
| 8595 | 8705 | } |
| 8596 | 8706 | |
| 8597 | - // swipe_id should be set if alternate greetings are added | |
| 8707 | +/** | |
| 8598 | - if (chat.length == 1 && chat[0].swipe_id === undefined) { | |
| 8708 | + * Returns the message's behavior when swiped past it's last branch. | |
| 8709 | + * This does not check if the message can currently be swiped. See isMessageSwipeable(). | |
| 8710 | + * This does not check if messages are generally swipeable. See isSwipingAllowed(). | |
| 8711 | + * This does not check if the swipes exist or are valid. | |
| 8712 | + * @param {number} messageId The message Id to check. | |
| 8713 | + * @param {ChatMessage} [message=undefined] If defined, this will be used instead of chat[messageId]. | |
| 8714 | + * @returns {OVERSWIPE_BEHAVIOR} | |
| 8715 | + */ | |
| 8716 | +export function getOverswipeBehavior(messageId, message = undefined) { | |
| 8717 | + message ??= chat[messageId]; | |
| 8718 | + | |
| 8719 | + const isPristine = !chat_metadata?.tainted; | |
| 8720 | + const isGreeting = messageId === 0; | |
| 8721 | + | |
| 8722 | + //Do not override explicitly set overswipe_behavior. | |
| 8723 | + if (typeof message?.extra?.overswipe_behavior == 'string') return message.extra.overswipe_behavior; | |
| 8724 | + //Some messages, like the welcome screen, are not swipeable. | |
| 8725 | + else if (message?.extra?.swipeable === false) return OVERSWIPE_BEHAVIOR.NONE; | |
| 8726 | + //Small System messages can't be swiped. | |
| 8727 | + else if (message?.extra?.isSmallSys) return OVERSWIPE_BEHAVIOR.NONE; | |
| 8728 | + //The first message in a priistine chat will loop. It's chevrons will always be visible https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373 | |
| 8729 | + else if (isGreeting && isPristine) return OVERSWIPE_BEHAVIOR.PRISTINE_GREETING; | |
| 8730 | + //Non-user and non-prompt hidden messages will regenerate. | |
| 8731 | + else if (!message?.is_user && !message?.is_system) return OVERSWIPE_BEHAVIOR.REGENERATE; | |
| 8732 | + //By default, all other messages will loop. Their swipe chevrons will only be shown if there is more than one swipe. | |
| 8733 | + else { return OVERSWIPE_BEHAVIOR.LOOP; } | |
| 8734 | +} | |
| 8735 | + | |
| 8736 | +/** | |
| 8737 | + * Refreshes all swipe buttons and updates their swipe counters. | |
| 8738 | + * This has been optimized for bulk updates by minimizing DOM queries. | |
| 8739 | + * @param {boolean} updateCounters When true, the swipe counters will also be updated. Typically redundant because addOneMessage updates the counters. | |
| 8740 | + * @param {boolean} fade By default, the chevrons fade in and out. | |
| 8741 | + * @returns | |
| 8742 | + */ | |
| 8743 | +export function refreshSwipeButtons(updateCounters = false, fade = true) { | |
| 8744 | + //Never show swipe buttons on an empty chat. | |
| 8745 | + if (chat?.length === 0) return false; | |
| 8746 | + | |
| 8747 | + //If swipes are disabled or hidden, hide all swipe buttons. | |
| 8748 | + if (!isSwipingAllowed()) { | |
| 8749 | + $('body').addClass('hideAllSwipeButtons'); | |
| 8599 | 8750 | return; |
| 8751 | + //Don't hide all swipe buttons. | |
| 8752 | + } else { | |
| 8753 | + //CSS will hide all messages. | |
| 8754 | + $('body').removeClass('hideAllSwipeButtons'); | |
| 8600 | 8755 | } |
| 8756 | + //Non-messages can appear in chat. '.mes' is required. | |
| 8757 | + const messageElements = chatElement.children('.mes[mesid]'); | |
| 8601 | 8758 | |
| 8602 | - //had to add this to make the swipe counter work | |
| 8759 | + const firstDisplayedMesId = Number(messageElements.first().attr('mesid')); | |
| 8603 | - //(copied from the onclick functions for swipe buttons.. | |
| 8604 | - //don't know why the array isn't set for non-swipe messages in Generate or addOneMessage..) | |
| 8605 | - if (chat[mesId]['swipe_id'] === undefined) { // if there is no swipe-message in the last spot of the chat array | |
| 8606 | - chat[mesId]['swipe_id'] = 0; // set it to id 0 | |
| 8607 | - chat[mesId]['swipes'] = []; // empty the array | |
| 8608 | - chat[mesId]['swipes'][0] = chat[mesId]['mes']; //assign swipe array with last message from chat | |
| 8609 | - chat[mesId]['swipe_info'] = []; | |
| 8610 | - chat[mesId]['swipe_info'][0] = { | |
| 8611 | - 'send_date': chat[mesId]['send_date'], | |
| 8612 | - 'gen_started': chat[mesId]['gen_started'], | |
| 8613 | - 'gen_finished': chat[mesId]['gen_finished'], | |
| 8614 | - 'extra': structuredClone(chat[mesId]['extra']), | |
| 8615 | - }; | |
| 8616 | - } | |
| 8617 | 8760 | |
| 8618 | - const currentMessage = chatElement.children().filter(`[mesid="${mesId}"]`); | |
| 8761 | + //Group each message. | |
| 8619 | - const swipeId = chat[mesId].swipe_id; | |
| 8762 | + messageElements.each((index, div) => { | |
| 8620 | - const swipeCounterText = formatSwipeCounter((swipeId + 1), chat[mesId]['swipes'].length); | |
| 8763 | + //This assumes the messages are in order and their Id's are accurate. | |
| 8621 | - const swipeRight = currentMessage.find('.swipe_right'); | |
| 8764 | + const messageId = firstDisplayedMesId + index; | |
| 8622 | - const swipeLeft = currentMessage.find('.swipe_left'); | |
| 8765 | + //Number($(div).attr('mesid')); Would not misscount due to a missing div, but is much slower. | |
| 8623 | - const swipeCounter = currentMessage.find('.swipes-counter'); | |
| 8624 | 8766 | |
| 8625 | - if (swipeId !== undefined && (chat[mesId].swipes.length > 1 || swipeId > 0)) { | |
| 8767 | + const message = chat[messageId]; | |
| 8626 | - swipeLeft.css('display', 'flex'); | |
| 8768 | + | |
| 8627 | - } | |
| 8769 | + //Chevrons should not fade-in during printMessages. //https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3539315919 | |
| 8628 | - //only show right when generate is off, or when next right swipe would not make a generate happen | |
| 8770 | + div.classList.toggle('fade', fade); | |
| 8629 | - if (is_send_press === false || chat[mesId].swipes.length >= swipeId) { | |
| 8771 | + | |
| 8630 | - swipeRight.css('display', 'flex').css('opacity', '0.3'); | |
| 8772 | + if (isMessageSwipeable(messageId, message)) { | |
| 8631 | - swipeCounter.css('opacity', '0.3'); | |
| 8773 | + //If a right swipe would trigger a generation or loop to the first swipe. | |
| 8774 | + const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0); | |
| 8775 | + const hasSwipes = (message?.swipes?.length > 1); | |
| 8776 | + const overswipe = getOverswipeBehavior(messageId, message); | |
| 8777 | + | |
| 8778 | + // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373 | |
| 8779 | + const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING; | |
| 8780 | + | |
| 8781 | + //The swipe button will be shown if an overswipe would trigger REGENERATE or EDIT_GENERATE. | |
| 8782 | + const isOverswipeable = isLastSwipe && | |
| 8783 | + overswipe == OVERSWIPE_BEHAVIOR.REGENERATE || | |
| 8784 | + overswipe == OVERSWIPE_BEHAVIOR.EDIT_GENERATE; | |
| 8785 | + | |
| 8786 | + div.classList.toggle('last_swipe', isOverswipeable); | |
| 8787 | + | |
| 8788 | + //If there's only one swipe, the left arrow should not be shown. | |
| 8789 | + div.classList.toggle('swipes_visible', hasSwipes || pristineGreeting); | |
| 8790 | + | |
| 8791 | + //updateSwipeCounter does not need to be awaited, It can run a bit later. | |
| 8792 | + if (updateCounters) updateSwipeCounter(messageId, { message, messageElement: $(div) }); | |
| 8793 | + } else { | |
| 8794 | + //Hide all messages that are not swipeable. | |
| 8795 | + div.classList.remove('swipes_visible', 'last_swipe'); | |
| 8632 | 8796 | } |
| 8633 | - if ((chat[mesId].swipes.length - swipeId) === 1) { | |
| 8797 | + }); | |
| 8634 | - //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not | |
| 8635 | - //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on | |
| 8636 | - swipeRight.css('opacity', '0.7'); | |
| 8637 | - swipeCounter.css('opacity', '0.7'); | |
| 8638 | 8798 | } |
| 8639 | - | |
| 8799 | +/** | |
| 8640 | - //allows for writing individual swipe counters for past messages | |
| 8800 | + * This function is misleadingly named. It allows generation then refreshes the swipe buttons and counters. | |
| 8641 | - const lastSwipeCounter = $('.last_mes .swipes-counter'); | |
| 8801 | + */ | |
| 8642 | - lastSwipeCounter.text(swipeCounterText).show(); | |
| 8802 | +export function showSwipeButtons() { | |
| 8803 | + swipesHidden = false; | |
| 8804 | + refreshSwipeButtons(); | |
| 8643 | 8805 | } |
| 8644 | 8806 | |
| 8645 | 8807 | /** |
| 8808 | + * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters. | |
| 8646 | 8809 | * @param {object} [options] Options |
| 8647 | 8810 | * @param {boolean} [options.hideCounters=false] Also hide the swipes counter. |
| 8648 | 8811 | */ |
| 8649 | 8812 | export function hideSwipeButtons({ hideCounters = false } = {}) { |
| 8650 | 8813 | isSwipingAllowedswipesHidden = falsetrue; |
| 8651 | - chatElement.find('.swipe_right').hide(); | |
| 8814 | + refreshSwipeButtons(); | |
| 8652 | - chatElement.find('.swipe_left').hide(); | |
| 8815 | + | |
| 8653 | 8816 | if (hideCounters === true) { |
| 8654 | 8817 | chatElement.find('.last_mes .swipes-counter').hideprop('hidden', true); |
| 8655 | 8818 | } |
| 8656 | 8819 | } |
| 8657 | 8820 | |
| @@ -8694,14 +8857,17 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) { | ||
| 8694 | 8857 | |
| 8695 | 8858 | // Select the next swipe, or the one before if it was the last one |
| 8696 | 8859 | const newSwipeId = Math.min(swipeId, message.swipes.length - 1); |
| 8697 | - syncSwipeToMes(messageId, newSwipeId); | |
| 8698 | 8860 | |
| 8699 | 8861 | chat_metadata['tainted'] = true; |
| 8700 | 8862 | |
| 8863 | + messageId = Number(messageId); | |
| 8864 | + swipeId = Number(swipeId); | |
| 8701 | 8865 | await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId }); |
| 8866 | + let direction = (swipeId <= newSwipeId) ? SWIPE_DIRECTION.RIGHT : SWIPE_DIRECTION.LEFT; | |
| 8867 | + //Animate swipe and swap dispayed message. | |
| 8868 | + await swipe(null, direction, { source: SWIPE_SOURCE.DELETE, repeated: false, forceMesId: messageId, forceSwipeId: newSwipeId }); | |
| 8702 | 8869 | |
| 8703 | 8870 | await saveChatConditional(); |
| 8704 | - await reloadCurrentChat(); | |
| 8705 | 8871 | |
| 8706 | 8872 | return newSwipeId; |
| 8707 | 8873 | } |
| @@ -8791,7 +8957,7 @@ export function updateEditArrowClasses() { | ||
| 8791 | 8957 | return; |
| 8792 | 8958 | } |
| 8793 | 8959 | |
| 8794 | 8960 | const message = chatElement.findchildren('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`); |
| 8795 | 8961 | |
| 8796 | 8962 | const downButton = message.find('.mes_edit_down'); |
| 8797 | 8963 | const upButton = message.find('.mes_edit_up'); |
| @@ -9226,6 +9392,23 @@ export async function createOrEditCharacter(e) { | ||
| 9226 | 9392 | } |
| 9227 | 9393 | |
| 9228 | 9394 | /** |
| 9395 | + * Visually updates all chat messages including andd after index by removing them, then adding them. | |
| 9396 | + * @param {ChatMessage[]} chat All messages in chat before index will remain unchanged. | |
| 9397 | + * @param {Number} index The last unchanged messageId. | |
| 9398 | + */ | |
| 9399 | +export async function redisplayChat(chat, index) { | |
| 9400 | + //Remove messages after index. | |
| 9401 | + chatElement.children(`.mes[mesid="${index}"]`).nextAll('.mes').addBack().remove(); | |
| 9402 | + | |
| 9403 | + //Skip to index, then add extra messages. | |
| 9404 | + for (let i = index; i <= chat.length - 1; i++) { | |
| 9405 | + //addOneMessage will update last_mes. | |
| 9406 | + addOneMessage(chat[i], { scroll: false, showSwipes: false, forceId: i }); | |
| 9407 | + } | |
| 9408 | + refreshSwipeButtons(); | |
| 9409 | +} | |
| 9410 | + | |
| 9411 | +/** | |
| 9229 | 9412 | * Formats a counter for a swipe view. |
| 9230 | 9413 | * @param {number} current The current number of items. |
| 9231 | 9414 | * @param {number} total The total number of items. |
| @@ -9240,41 +9423,61 @@ function formatSwipeCounter(current, total) { | ||
| 9240 | 9423 | |
| 9241 | 9424 | /** |
| 9242 | 9425 | * Handles the swipe event. |
| 9243 | 9426 | * @param {JQuery.EventSwipeEvent} _eventevent Event. |
| 9244 | 9427 | * @param {'left'|'right'} direction The direction to swipe. |
| 9245 | 9428 | * @param {object} params Additional parameters. |
| 9246 | 9429 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9247 | 9430 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9248 | 9431 | * @param {objectChatMessage} [params.message=chat[chat.length - 1]] The chat message to swipe. |
| 9432 | + * @param {number} [params.forceMesId] The message id to swipe. | |
| 9433 | + * @param {number} [params.forceSwipeId] The target swipe_id. When out of range, it will be looped or clamped. | |
| 9434 | + * @param {number} [params.forceDuration] Overwrites the default swipe duration. | |
| 9249 | 9435 | */ |
| 9250 | 9436 | export async function swipe(_eventevent, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) { |
| 9251 | 9437 | if (chat.length === 0) { |
| 9252 | 9438 | console.warn('Swipe was called on an empty chat.'); |
| 9253 | 9439 | return; |
| 9254 | 9440 | } |
| 9255 | 9441 | |
| 9256 | - //Only allow one concurrent swipe. | |
| 9257 | - if (!isSwipingAllowed) { | |
| 9258 | - console.info('The swipe has been ignored because another is in progress.'); | |
| 9259 | - return; | |
| 9260 | - } | |
| 9261 | - isSwipingAllowed = false; | |
| 9262 | - | |
| 9263 | - let generation; | |
| 9264 | 9442 | let messageIndex; |
| 9265 | 9443 | |
| 9266 | 9444 | //Only set messageIndex if message exists because -1 is truthy. |
| 9267 | 9445 | if (message) { |
| 9268 | 9446 | messageIndex = chat.indexOf(message); |
| 9269 | 9447 | if (messageIndex === -1 && typeof (forceMesId) != 'number') { |
| 9270 | 9448 | console.error(`The message must exist in chat. ${message};`); |
| 9271 | 9449 | return; |
| 9272 | 9450 | } |
| 9273 | 9451 | } |
| 9274 | 9452 | |
| 9275 | 9453 | const mesId = Number($(this)forceMesId ?? event?.currentTarget?.closest('.mes')?.attrgetAttribute('mesid') ?? messageIndex ?? chat.length - 1); |
| 9454 | + | |
| 9455 | + if (source === SWIPE_SOURCE.DELETE || source === SWIPE_SOURCE.BACK || source === SWIPE_SOURCE.AUTO_SWIPE) { | |
| 9456 | + console.info(`The ${direction} swipe source on message #${mesId} is ${source}, Most checks have been bypassed. `); | |
| 9457 | + } else { | |
| 9458 | + //Only show an error if swipes are not hidden and a message is generating. | |
| 9459 | + if (isGenerating() && (swipes && !swipesHidden && (swipeState === SWIPE_STATE.NONE))) { | |
| 9460 | + toastr.warning(t`Cannot swipe while generating. Stop the request and try again.`, t`Swipe aborted`); | |
| 9461 | + return; | |
| 9462 | + } | |
| 9463 | + //Only allow one concurrent swipe. | |
| 9464 | + if (!isSwipingAllowed()) { | |
| 9465 | + console.info('The swipe has been ignored messages cannot currently be swiped.'); | |
| 9466 | + return; | |
| 9467 | + } | |
| 9468 | + if (!isMessageSwipeable(mesId, message)) { | |
| 9469 | + console.info(`Message #${mesId} cannot be swiped. ${message}`); | |
| 9470 | + return; | |
| 9471 | + } | |
| 9472 | + } | |
| 9473 | + | |
| 9474 | + // Cancel pending save to prevent accidental swipe_id overwrites. | |
| 9475 | + cancelDebouncedChatSave(); | |
| 9476 | + | |
| 9477 | + swipeState = SWIPE_STATE.SWIPING; | |
| 9478 | + let generation; | |
| 9276 | 9479 | |
| 9277 | 9480 | const thisMesDiv = chatElement.children('.mes').filter(`.mes[mesid="${mesId}"]`); |
| 9278 | 9481 | const thisMesText = thisMesDiv.find('.mes_block .mes_text'); |
| 9279 | 9482 | const thisMesDivHeight = thisMesDiv[0]?.scrollHeight; |
| 9280 | 9483 | const thisMesTextHeight = thisMesText[0]?.scrollHeight; |
| @@ -9283,43 +9486,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9283 | 9486 | return; |
| 9284 | 9487 | } |
| 9285 | 9488 | const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0); |
| 9286 | 9489 | let newSwipeId = Number(forceSwipeId ?? originalSwipeId); |
| 9287 | 9490 | |
| 9288 | - const isPristine = !chat_metadata?.tainted; | |
| 9491 | + /** | |
| 9289 | - const swipeDuration = Math.round(animation_duration * 1.25); | |
| 9492 | + * Calculates the next swipe duration with how many swipes have been repeated. | |
| 9290 | - const swipeRange = direction === SWIPE_DIRECTION.RIGHT ? -700 : 700; | |
| 9493 | + * @param {number} animation_duration | |
| 9494 | + * @returns {number} The adjusted swipe duration. | |
| 9495 | + */ | |
| 9496 | + function getSwipeDuration(animation_duration) { | |
| 9497 | + const now = performance.now(); | |
| 9498 | + const resetTime = animation_duration * 2 + 300; | |
| 9499 | + | |
| 9500 | + //Reset the counter if the last swipe was more than half a second ago. | |
| 9501 | + if (now - lastSwipeInfo.now >= resetTime || direction !== lastSwipeInfo.direction) recentSwipes = 0; | |
| 9502 | + recentSwipes++; | |
| 9503 | + lastSwipeInfo = { now, direction }; | |
| 9504 | + | |
| 9505 | + //At 4 swipes, animation_duration will be halved. | |
| 9506 | + const sigmoid = 1 / (1 + Math.exp(recentSwipes - 4)); | |
| 9507 | + | |
| 9508 | + return animation_duration * sigmoid; | |
| 9509 | + } | |
| 9291 | 9510 | |
| 9292 | - async function endSwipe() { | |
| 9511 | + const swipeDuration = forceDuration ?? getSwipeDuration(animation_duration); | |
| 9512 | + | |
| 9513 | + //The offscreen messages may be visible if the user resizes the viewport during a swipe. | |
| 9514 | + const thisMesDivWidth = thisMesDiv.width() + 30; | |
| 9515 | + let swipeRange = (direction === SWIPE_DIRECTION.RIGHT) ? -thisMesDivWidth : thisMesDivWidth; | |
| 9516 | + | |
| 9517 | + /** | |
| 9518 | + * Waits for the generation to end, reverts the swipe if swipe_id has not changed. | |
| 9519 | + * @param {boolean} revert Attept to revert the swipe without saving. | |
| 9520 | + */ | |
| 9521 | + async function endSwipe(revert = false) { | |
| 9293 | 9522 | //Wait for the generation to end. |
| 9294 | 9523 | try { |
| 9524 | + //`mes_buttons` need to be hidden until the animation completes. | |
| 9525 | + if (generation) { | |
| 9526 | + document.body.dataset.swiping = 'true'; | |
| 9295 | 9527 | await generation; |
| 9296 | 9528 | } |
| 9529 | + } | |
| 9297 | 9530 | catch (error) { |
| 9298 | 9531 | console.warn(`Swipe failed, Swiping back. ${error}`); |
| 9299 | 9532 | } |
| 9300 | - //Allow for another swipe. | |
| 9301 | - showSwipeButtons(); | |
| 9302 | 9533 | |
| 9303 | 9534 | //Clamp Id between swipes. |
| 9304 | 9535 | let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1)); |
| 9305 | 9536 | |
| 9306 | - //If the id is not within bounds, Swipe back. | |
| 9307 | - if (chat[mesId]['swipe_id'] !== clampedId) { | |
| 9308 | - chat[mesId]['swipe_id'] = clampedId; | |
| 9309 | - syncSwipeToMes(mesId); | |
| 9310 | - addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: true }); | |
| 9311 | - } | |
| 9312 | - | |
| 9313 | 9537 | await updateSwipeCounter(mesId); |
| 9314 | 9538 | //Fallback. |
| 9315 | 9539 | if (mesId != chat.length - 1) { |
| 9316 | 9540 | await updateSwipeCounter(chat.length - 1); |
| 9317 | 9541 | } |
| 9542 | + | |
| 9543 | + // If swipe_id has not changed, give the user feedback. | |
| 9544 | + if (clampedId == originalSwipeId && source != SWIPE_SOURCE.DELETE) { | |
| 9545 | + try { | |
| 9546 | + //Shake 700/140=5px | |
| 9547 | + shakeElement(thisMesDiv, -swipeRange / 140, animation_duration, 'ease-in'); | |
| 9548 | + //Flash red. | |
| 9549 | + const flashTime = Math.max(animation_duration * 2, 100); | |
| 9550 | + await Promise.race([thisMesDiv.find('.swipes-counter').animate({ color: 'red' }, flashTime).animate({ color: '' }).promise(), createTimeout(flashTime * 4, `The shake animation did not end within ${flashTime * 4}ms`)].filter(Boolean)); | |
| 9551 | + } catch (error) { | |
| 9552 | + console.warn(error); | |
| 9553 | + } | |
| 9554 | + } | |
| 9555 | + | |
| 9556 | + //If the id is not within bounds, Swipe back. | |
| 9557 | + if (chat[mesId]?.swipe_id !== clampedId || revert) { | |
| 9558 | + // Prevent recursion. | |
| 9559 | + if (source != SWIPE_SOURCE.BACK) { | |
| 9560 | + source = SWIPE_SOURCE.BACK; | |
| 9561 | + chat[mesId].swipe_id = clampedId; | |
| 9562 | + | |
| 9563 | + //Update the chat. | |
| 9564 | + await loadFromSwipeId(mesId, chat[mesId].swipe_id); | |
| 9565 | + await redisplayChat(chat, mesId); | |
| 9566 | + } | |
| 9567 | + else { | |
| 9568 | + await Popup.show.confirm( | |
| 9569 | + t`ERROR: <code>syncSwipeToMes</code> has failed to revert the failed ${direction} swipe on message #${mesId}.`, | |
| 9570 | + t`<p>After you click OK, the chat will be reloaded to prevent data corruption.</p>`, | |
| 9571 | + { okButton: 'OK', cancelButton: false }, | |
| 9572 | + ); | |
| 9573 | + console.trace(`Error! Recursion detected when reverting failed ${direction} swipe on message #${mesId}. Something has broken.`); | |
| 9574 | + await reloadCurrentChat(); | |
| 9575 | + } | |
| 9576 | + //Out of bounds swipes should not be saved. | |
| 9577 | + } else if (source != SWIPE_SOURCE.BACK) { | |
| 9578 | + //Save the chat if swipe_id has changed. | |
| 9579 | + saveChatDebounced(); | |
| 9318 | 9580 | } |
| 9319 | 9581 | |
| 9320 | - async function standardSwipe() { | |
| 9582 | + //Allow for another swipe. | |
| 9583 | + swipeState = SWIPE_STATE.NONE; | |
| 9584 | + delete document.body.dataset.swiping; | |
| 9585 | + showSwipeButtons(); | |
| 9586 | + } | |
| 9587 | + | |
| 9588 | + async function standardSwipe(newSwipeId) { | |
| 9321 | 9589 | //If swipe_id has changed, or the source is being deleted. |
| 9322 | 9590 | if (newSwipeId !== originalSwipeId || source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) { |
| 9323 | 9591 | //Update the chat. |
| 9324 | 9592 | await loadFromSwipeId(mesId, newSwipeId); |
| 9325 | 9593 | //Transition to the new chat. |
| @@ -9329,6 +9597,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9329 | 9597 | } |
| 9330 | 9598 | |
| 9331 | 9599 | /** |
| 9600 | + * Removes a message's extra and gen times. | |
| 9601 | + * @param {ChatMessage} message | |
| 9602 | + */ | |
| 9603 | + function clearMessageData(message) { | |
| 9604 | + if (message.extra && typeof message.extra === 'object') { | |
| 9605 | + delete message.extra.memory; | |
| 9606 | + delete message.extra.display_text; | |
| 9607 | + delete message.extra.media; | |
| 9608 | + delete message.extra.inline_image; | |
| 9609 | + delete message.extra.files; | |
| 9610 | + delete message.extra.fileLength; | |
| 9611 | + delete message.extra.generationType; | |
| 9612 | + delete message.extra.negative; | |
| 9613 | + delete message.extra.title; | |
| 9614 | + delete message.extra.append_title; | |
| 9615 | + } | |
| 9616 | + delete message.gen_started; | |
| 9617 | + delete message.gen_finished; | |
| 9618 | + } | |
| 9619 | + | |
| 9620 | + /** | |
| 9332 | 9621 | * Sets the message to the newSwipeId and loads it. |
| 9333 | 9622 | * @param {number} mesId |
| 9334 | 9623 | * @param {number} newSwipeId |
| @@ -9337,55 +9626,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9337 | 9626 | //Update the swipe_id. |
| 9338 | 9627 | chat[mesId]['swipe_id'] = newSwipeId; |
| 9339 | 9628 | |
| 9340 | - if (chat[mesId].extra && typeof chat[mesId].extra === 'object') { | |
| 9629 | + clearMessageData(chat[mesId]); | |
| 9341 | - delete chat[mesId].extra.memory; | |
| 9630 | + | |
| 9342 | - delete chat[mesId].extra.display_text; | |
| 9631 | + //Load from swipes. | |
| 9343 | - delete chat[mesId].extra.media; | |
| 9632 | + if (syncSwipeToMes(mesId, newSwipeId) == false) { | |
| 9344 | - delete chat[mesId].extra.inline_image; | |
| 9633 | + let errorMessage = t`When swiping ${direction} on message ${mesId}, syncSwipeToMes has returned false. Attempting to swipe back!`; | |
| 9345 | - delete chat[mesId].extra.files; | |
| 9634 | + toastr.error(errorMessage); | |
| 9346 | - delete chat[mesId].extra.fileLength; | |
| 9635 | + | |
| 9347 | 9636 | delete chat[mesId].extra.generationTypeswipe_id = originalSwipeId; |
| 9348 | - delete chat[mesId].extra.negative; | |
| 9637 | + await endSwipe(true); | |
| 9349 | - delete chat[mesId].extra.title; | |
| 9638 | + } | |
| 9350 | - delete chat[mesId].extra.append_title; | |
| 9639 | + return true; | |
| 9351 | 9640 | } |
| 9352 | - delete chat[mesId].gen_started; | |
| 9353 | - delete chat[mesId].gen_finished; | |
| 9354 | - //load from swipes. | |
| 9355 | - syncSwipeToMes(mesId, chat[mesId]['swipe_id']); | |
| 9356 | - } | |
| 9357 | - | |
| 9358 | - // Helper function to convert transition to promise | |
| 9359 | - const transitionPromise = (element, properties) => { | |
| 9360 | - return new Promise((resolve) => { | |
| 9361 | - element.transition({ | |
| 9362 | - ...properties, | |
| 9363 | - complete: resolve, | |
| 9364 | - }); | |
| 9365 | - }); | |
| 9366 | - }; | |
| 9367 | 9641 | |
| 9368 | 9642 | /** |
| 9369 | 9643 | * Animates a swipe for all messages >= mesId. |
| 9370 | 9644 | * @param {number} mesId |
| 9371 | 9645 | * @param {numberobject} xparams |
| 9372 | 9646 | * @param {numberstring} duration[params.xStart='opx'] |
| 9373 | - */ | |
| 9647 | + * @param {string} [params.xEnd='0px'] | |
| 9374 | - async function animateSwipeTransition(mesId, x, duration) { | |
| 9648 | + * @param {number} [params.duration=animation_duration] | |
| 9375 | - //Selects the swiped message. | |
| 9649 | + * @param {string} [params.classes=''] Additional CSS classes to target during the swipe. | |
| 9376 | - const swipedMessagesDiv = chatElement.children().filter((index, div) => { | |
| 9650 | + * @param {boolean} [params.freeze=true] When true, do not remove the class from the animation, leaving it stuck at xEnd. | |
| 9377 | - const $div = $(div); | |
| 9651 | + * @returns {Promise<boolean|Function>} endSlide unfreezes the messages from xEnd. | |
| 9378 | - return mesId === Number($div.attr('mesid')); | |
| 9652 | + */ | |
| 9379 | - }); | |
| 9653 | + async function animateSwipeTransition(mesId, { xStart = '0px', xEnd = '0px', duration = animation_duration, classes = '', freeze = false } = {}) { | |
| 9380 | - const swipedElementsDiv = swipedMessagesDiv.children('.mes_block, .mesAvatarWrapper'); | |
| 9654 | + // If the animation_duration is zero, the 'animationend' promise will never resolve. | |
| 9655 | + //Skip the animation if it's faster than 50ms. | |
| 9656 | + if (duration <= 50) return; | |
| 9657 | + | |
| 9658 | + //Select MAXIMUM_ANIMATED messages after mesId. Ideally, only visible messages would be animated. | |
| 9659 | + const MAXIMUM_ANIMATED = 100; | |
| 9660 | + | |
| 9661 | + const messages = chatElement.children('.mes'); | |
| 9662 | + const firstDisplayedMesId = Number(messages.first().attr('mesid')); | |
| 9663 | + | |
| 9664 | + const swipedMessagesDiv = messages.filter((index, div) => { | |
| 9665 | + // const messageId = Number($(div).attr('mesid')); //Slower. | |
| 9666 | + //This assumes the messages are in order and their Id's are accurate. | |
| 9667 | + const divMessageId = firstDisplayedMesId + index; | |
| 9668 | + | |
| 9669 | + return (divMessageId < mesId + MAXIMUM_ANIMATED && divMessageId >= mesId); | |
| 9670 | + }); | |
| 9671 | + if (swipedMessagesDiv.length > 0) { | |
| 9672 | + let swipeClasses = '.mes_block, .mesAvatarWrapper'; | |
| 9673 | + swipeClasses += classes; | |
| 9674 | + | |
| 9675 | + //Select only the target classes. | |
| 9676 | + const swipedElementsDiv = swipedMessagesDiv.children(swipeClasses); | |
| 9677 | + if (swipedElementsDiv.length > 0) { | |
| 9678 | + //This is a global variable, only one swipe transition can occur concurrently. | |
| 9679 | + document.documentElement.style.setProperty('--slide-mes-x-start', xStart); | |
| 9680 | + document.documentElement.style.setProperty('--slide-mes-x-end', xEnd); | |
| 9681 | + document.documentElement.style.setProperty('--slide-mes-x-duration', `${duration}ms`); | |
| 9682 | + | |
| 9683 | + //The class must be removed to unfreze previous slides. | |
| 9684 | + swipedElementsDiv.removeClass('slide'); | |
| 9685 | + //CSS starts the animation. | |
| 9686 | + void swipedElementsDiv[0].offsetWidth; | |
| 9687 | + swipedElementsDiv.addClass('slide'); | |
| 9688 | + | |
| 9689 | + const endSlide = () => { | |
| 9690 | + //Remove the style when done. | |
| 9691 | + swipedElementsDiv.removeClass('slide'); | |
| 9692 | + | |
| 9693 | + document.documentElement.style.setProperty('--slide-mes-x-start', ''); | |
| 9694 | + document.documentElement.style.setProperty('--slide-mes-x-end', ''); | |
| 9695 | + document.documentElement.style.setProperty('--slide-mes-duration', ''); | |
| 9696 | + return true; | |
| 9697 | + }; | |
| 9698 | + //Wait for the animation's end. https://developer.mozilla.org/en-US/docs/Web/API/Animation/finished | |
| 9699 | + const animation = swipedElementsDiv[0]?.getAnimations().filter((a) => a['animationName'] == 'slide')[0]; | |
| 9700 | + try { | |
| 9701 | + await Promise.race([animation?.finished, createTimeout(duration * 2, `The ${duration}ms swipe animation has not ended after ${duration * 2}ms. It has been skipped.`)].filter(Boolean)); | |
| 9702 | + } catch (error) { | |
| 9703 | + console.warn(error); | |
| 9704 | + } | |
| 9381 | 9705 | |
| 9382 | - //Swipe. | |
| 9706 | + //If not frozen, end the slide now. | |
| 9383 | - await transitionPromise(swipedElementsDiv, { | |
| 9707 | + return freeze ? endSlide : endSlide(); | |
| 9384 | - x: x, | |
| 9708 | + } | |
| 9385 | - duration: duration, | |
| 9709 | + } | |
| 9386 | - easing: animation_easing, | |
| 9710 | + console.warn(`No animatable messages were found after message #${mesId}.`); | |
| 9387 | - queue: false, | |
| 9711 | + return false; | |
| 9388 | - }); | |
| 9389 | 9712 | } |
| 9390 | 9713 | |
| 9391 | 9714 | function getMessageBottomHeight(thisMesDiv) { |
| @@ -9427,11 +9750,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9427 | 9750 | /** |
| 9428 | 9751 | * Anime a swipe, optionally running a generation. |
| 9429 | 9752 | * @param {boolean} run_generate |
| 9753 | + * @param {boolean} [skipSwipeOut=false] | |
| 9430 | 9754 | */ |
| 9431 | 9755 | async function animateSwipe(run_generate = false, skipSwipeOut = false) { |
| 9432 | 9756 | |
| 9757 | + if (!skipSwipeOut) { | |
| 9433 | 9758 | //Swipe out. |
| 9434 | 9759 | await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration }); |
| 9760 | + } | |
| 9761 | + | |
| 9435 | 9762 | |
| 9436 | 9763 | if (run_generate) { |
| 9437 | 9764 | await updateSwipeCounter(mesId); |
| @@ -9447,7 +9774,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9447 | 9774 | |
| 9448 | 9775 | //Only scroll when swiping the last message. |
| 9449 | 9776 | const scroll = (mesId == chat.length - 1); |
| 9450 | - addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll }); | |
| 9777 | + //The swipe buttons will be refreshed in endSwipe(), refreshing them now will cause flickering. | |
| 9778 | + addOneMessage(chat[mesId], { type: 'swipe', forceId: mesId, scroll: scroll, showSwipes: false }); | |
| 9451 | 9779 | |
| 9452 | 9780 | if (power_user.message_token_count_enabled) { |
| 9453 | 9781 | if (!chat[mesId].extra) { |
| @@ -9465,10 +9793,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9465 | 9793 | thisMesDiv.css('height', thisMesDivHeight); |
| 9466 | 9794 | expandNewMessage(thisMesDiv); |
| 9467 | 9795 | |
| 9468 | - | |
| 9469 | - //Jump to the opposite side. | |
| 9470 | - await animateSwipeTransition(mesId, -swipeRange, 0); | |
| 9471 | - | |
| 9472 | 9796 | appendMediaToMessage(chat[mesId], thisMesDiv); |
| 9473 | 9797 | |
| 9474 | 9798 | await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId)); |
| @@ -9476,12 +9800,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9476 | 9800 | if (run_generate && !is_send_press) { |
| 9477 | 9801 | is_send_press = true; |
| 9478 | 9802 | generation = Generate('swipe'); |
| 9479 | - } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) { | |
| 9480 | - saveChatDebounced(); | |
| 9481 | 9803 | } |
| 9482 | 9804 | |
| 9483 | 9805 | //Swipe in from the opposite side. |
| 9484 | - await animateSwipeTransition(mesId, 0, swipeDuration); | |
| 9806 | + await animateSwipeTransition(mesId, { xStart: `${-swipeRange}px`, xEnd: `${0}px`, duration: swipeDuration }); | |
| 9485 | 9807 | } |
| 9486 | 9808 | |
| 9487 | 9809 | if (mesId === Number(this_edit_mes_id)) { |
| @@ -9496,7 +9818,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9496 | 9818 | } |
| 9497 | 9819 | |
| 9498 | 9820 | //If the swipe is not being deleted. |
| 9499 | 9821 | if (source != 'delete'SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) { |
| 9500 | 9822 | |
| 9501 | 9823 | // Make sure ad-hoc changes to extras are saved before swiping away |
| 9502 | 9824 | syncMesToSwipe(mesId); |
| @@ -9515,19 +9837,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9515 | 9837 | } |
| 9516 | 9838 | // If the user is holding down the key and we're at the last or first swipe, don't do anything. |
| 9517 | 9839 | let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0; |
| 9518 | 9840 | if (source === 'keyboard'SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) { |
| 9519 | 9841 | await endSwipe(); |
| 9520 | 9842 | return; |
| 9521 | 9843 | } |
| 9522 | 9844 | } else if (source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) { |
| 9523 | 9845 | //If the swipe is being deleted or reverted. |
| 9524 | 9846 | await standardSwipe(newSwipeId); |
| 9525 | 9847 | return; |
| 9526 | 9848 | } |
| 9527 | 9849 | |
| 9528 | 9850 | //If swiping left. |
| 9529 | 9851 | if (direction === SWIPE_DIRECTION.LEFT) { |
| 9530 | - newSwipeId--; | |
| 9852 | + if (forceSwipeId == null) newSwipeId--; | |
| 9531 | 9853 | //Loop to last swipe if negative. |
| 9532 | 9854 | if (newSwipeId < 0) { |
| 9533 | 9855 | newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1); |
| @@ -9539,13 +9861,13 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9539 | 9861 | await endSwipe(); |
| 9540 | 9862 | return; |
| 9541 | 9863 | } |
| 9542 | 9864 | await standardSwipe(newSwipeId); |
| 9543 | 9865 | return; |
| 9544 | 9866 | } |
| 9545 | 9867 | //If swiping right. |
| 9546 | 9868 | else if (direction === SWIPE_DIRECTION.RIGHT) { |
| 9547 | 9869 | // make new slot in array |
| 9548 | - newSwipeId++; | |
| 9870 | + if (forceSwipeId == null) newSwipeId++; | |
| 9549 | 9871 | |
| 9550 | 9872 | //Minimum of zero. |
| 9551 | 9873 | if (newSwipeId < 0) { |
| @@ -9555,35 +9877,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9555 | 9877 | return; |
| 9556 | 9878 | } |
| 9557 | 9879 | |
| 9558 | - //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting. | |
| 9880 | + //If overswiping. | |
| 9559 | 9881 | if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) { |
| 9560 | 9882 | newSwipeId = chat[mesId]['swipes'].length; |
| 9561 | 9883 | |
| 9562 | 9884 | //Update the swipe_id. |
| 9563 | 9885 | chat[mesId]['swipe_id'] = newSwipeId; |
| 9564 | 9886 | |
| 9565 | - //Cancel the generation if it's a user message or the first message in a pristine chat. | |
| 9887 | + const overswipe = getOverswipeBehavior(mesId); | |
| 9566 | - if (chat[mesId].is_user || (mesId === 0 && isPristine)) { | |
| 9888 | + | |
| 9889 | + //Cancel the generation. | |
| 9890 | + if (overswipe == OVERSWIPE_BEHAVIOR.NONE) { | |
| 9567 | 9891 | //Cancel swipe. |
| 9568 | 9892 | chat[mesId]['swipe_id'] = originalSwipeId; |
| 9569 | 9893 | await endSwipe(); |
| 9570 | 9894 | return; |
| 9571 | 9895 | } else { |
| 9572 | - //Generate. | |
| 9896 | + //Regenerate the message | |
| 9573 | - await loadFromSwipeId(mesId, newSwipeId); | |
| 9897 | + else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) { | |
| 9898 | + clearMessageData(chat[mesId]); | |
| 9574 | 9899 | let run_generate = true; |
| 9900 | + //Generate. | |
| 9575 | 9901 | await animateSwipe(run_generate); |
| 9576 | 9902 | await endSwipe(); |
| 9577 | 9903 | return; |
| 9578 | 9904 | } |
| 9579 | - } | |
| 9905 | + // Loop to the first swipe. | |
| 9580 | - else { | |
| 9906 | + else if (overswipe == OVERSWIPE_BEHAVIOR.LOOP || overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING) { | |
| 9581 | - // if swipe_right is called on the last alternate greeting in pristine chats, loop back around | |
| 9582 | - if (chat.length === 1 && newSwipeId !== undefined && newSwipeId === chat[0]['swipes'].length && isPristine) { | |
| 9583 | 9907 | newSwipeId = 0; |
| 9584 | 9908 | } |
| 9585 | 9909 | } |
| 9586 | 9910 | await standardSwipe(newSwipeId); |
| 9587 | 9911 | return; |
| 9588 | 9912 | } |
| 9589 | 9913 | } |
| @@ -9591,28 +9915,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha | ||
| 9591 | 9915 | /** |
| 9592 | 9916 | * @deprecated Use `swipe` instead. |
| 9593 | 9917 | * Handles the swipe to the left event. |
| 9594 | 9918 | * @param {JQuery.EventSwipeEvent} _event[event] Event. |
| 9595 | 9919 | * @param {object} params Additional parameters. |
| 9596 | 9920 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9597 | 9921 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9598 | 9922 | * @param {object} [params.message] The chat message to swipe. |
| 9599 | 9923 | */ |
| 9600 | 9924 | export async function swipe_left(_eventevent, { source, repeated, message } = {}) { |
| 9601 | 9925 | await swipe.call(this, _eventevent, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message }); |
| 9602 | 9926 | } |
| 9603 | 9927 | |
| 9604 | 9928 | /** |
| 9605 | 9929 | * @deprecated Use `swipe` instead. |
| 9606 | 9930 | * Handles the swipe to the right event. |
| 9607 | 9931 | * @param {JQuery.EventSwipeEvent} [_eventevent] Event. |
| 9608 | 9932 | * @param {object} params Additional parameters. |
| 9609 | 9933 | * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'. |
| 9610 | 9934 | * @param {boolean} [params.repeated] Is the swipe event repeated. |
| 9611 | 9935 | * @param {object} [params.message] The chat message to swipe. |
| 9612 | 9936 | */ |
| 9613 | 9937 | //MARK: swipe_right |
| 9614 | 9938 | export async function swipe_right(_eventevent = null, { source, repeated, message } = {}) { |
| 9615 | 9939 | await swipe.call(this, _eventevent, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message }); |
| 9616 | 9940 | } |
| 9617 | 9941 | |
| 9618 | 9942 | /** |
| @@ -10782,10 +11106,12 @@ jQuery(async function () { | ||
| 10782 | 11106 | } |
| 10783 | 11107 | |
| 10784 | 11108 | else if (id == 'option_regenerate') { |
| 10785 | - closeMessageEditor(); | |
| 11109 | + //Attempting to regenerate a user message will instead generate a new message. | |
| 11110 | + if (chat.length && chat.length - 1 === this_edit_mes_id && chat[this_edit_mes_id]?.is_user == false) { | |
| 11111 | + toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot regenerate the message you are editing.`); | |
| 11112 | + return; | |
| 11113 | + } | |
| 10786 | 11114 | if (is_send_press == false) { |
| 10787 | - //hideSwipeButtons(); | |
| 10788 | - | |
| 10789 | 11115 | if (selected_group) { |
| 10790 | 11116 | regenerateGroup(); |
| 10791 | 11117 | } |
| @@ -10804,7 +11130,14 @@ jQuery(async function () { | ||
| 10804 | 11130 | } |
| 10805 | 11131 | |
| 10806 | 11132 | else if (id == 'option_continue') { |
| 10807 | - if (this_edit_mes_id >= 0) return; // don't proceed if editing a message | |
| 11133 | + if (swipeState == SWIPE_STATE.EDITING) { | |
| 11134 | + toastr.warning(t`Confirm the edit to start a generation.`, t`You cannot send a message during a swipe-edit.`); | |
| 11135 | + return; | |
| 11136 | + } | |
| 11137 | + if (chat.length && chat.length - 1 === this_edit_mes_id) { | |
| 11138 | + toastr.warning(t`Finish the edit before starting a generation.`, t`You cannot continue the message you are editing.`); | |
| 11139 | + return; | |
| 11140 | + } | |
| 10808 | 11141 | |
| 10809 | 11142 | if (is_send_press == false || fromSlashCommand) { |
| 10810 | 11143 | is_send_press = true; |
| @@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js'; | ||
| 37 | 37 | import { chat_completion_sources, oai_settings } from './openai.js'; |
| 38 | 38 | import { getTokenCountAsync } from './tokenizers.js'; |
| 39 | 39 | import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js'; |
| 40 | 40 | import { debounce_timeout, SWIPE_SOURCE } from './constants.js'; |
| 41 | 41 | |
| 42 | 42 | import { Popup } from './popup.js'; |
| 43 | 43 | import { accountStorage } from './util/AccountStorage.js'; |
| @@ -1106,7 +1106,7 @@ export function initRossMods() { | ||
| 1106 | 1106 | |
| 1107 | 1107 | if (event.key == 'ArrowLeft') { //swipes left |
| 1108 | 1108 | if ( |
| 1109 | 1109 | isSwipingAllowed() && |
| 1110 | 1110 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1111 | 1111 | $('#send_textarea').val() === '' && |
| 1112 | 1112 | $('#character_popup').css('display') === 'none' && |
| @@ -1114,13 +1114,13 @@ export function initRossMods() { | ||
| 1114 | 1114 | !isInputElementInFocus() && |
| 1115 | 1115 | !isModifiedKeyboardEvent(event) |
| 1116 | 1116 | ) { |
| 1117 | 1117 | $('.swipe_left:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1118 | 1118 | return; |
| 1119 | 1119 | } |
| 1120 | 1120 | } |
| 1121 | 1121 | if (event.key == 'ArrowRight') { //swipes right |
| 1122 | 1122 | if ( |
| 1123 | 1123 | isSwipingAllowed() && |
| 1124 | 1124 | !isNanogallery2LightboxActive() && // Check if lightbox is NOT active |
| 1125 | 1125 | $('#send_textarea').val() === '' && |
| 1126 | 1126 | $('#character_popup').css('display') === 'none' && |
| @@ -1128,7 +1128,7 @@ export function initRossMods() { | ||
| 1128 | 1128 | !isInputElementInFocus() && |
| 1129 | 1129 | !isModifiedKeyboardEvent(event) |
| 1130 | 1130 | ) { |
| 1131 | 1131 | $('.swipe_right:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat }); |
| 1132 | 1132 | return; |
| 1133 | 1133 | } |
| 1134 | 1134 | } |
| @@ -140,9 +140,46 @@ export const SCROLL_BEHAVIOR = { | ||
| 140 | 140 | }; |
| 141 | 141 | |
| 142 | 142 | /** |
| 143 | + * @enum {string} | |
| 144 | + * @readonly | |
| 145 | + */ | |
| 146 | +export const OVERSWIPE_BEHAVIOR = { | |
| 147 | + /** The overswipe right chevron will not be displayed. */ | |
| 148 | + NONE: 'none', | |
| 149 | + /** An overswipe will loop to the first swipe. */ | |
| 150 | + LOOP: 'loop', | |
| 151 | + /** Pristine greetings will loop, and chevrons will always be shown: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373 */ | |
| 152 | + PRISTINE_GREETING: 'pristine_greeting', | |
| 153 | + /** If chat tree is enabled, then an overswipe will allow the user to edit the message before starting a new generation. */ | |
| 154 | + EDIT_GENERATE: 'edit_generate', | |
| 155 | + /** This is the default behavior on character messages. */ | |
| 156 | + REGENERATE: 'regenerate', | |
| 157 | +}; | |
| 158 | + | |
| 159 | +/** | |
| 143 | 160 | * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}} |
| 144 | 161 | */ |
| 145 | 162 | export const SWIPE_DIRECTION = { |
| 146 | 163 | LEFT: 'left', |
| 147 | 164 | RIGHT: 'right', |
| 148 | 165 | }; |
| 166 | + | |
| 167 | +/** | |
| 168 | + * @type {{readonly DELETE: 'delete', readonly KEYBOARD: 'keyboard', readonly BACK: 'back', readonly AUTO_SWIPE: 'auto_swipe'}} | |
| 169 | + */ | |
| 170 | +export const SWIPE_SOURCE = { | |
| 171 | + DELETE: 'delete', | |
| 172 | + KEYBOARD: 'keyboard', | |
| 173 | + BACK: 'back', | |
| 174 | + AUTO_SWIPE: 'auto_swipe', | |
| 175 | +}; | |
| 176 | + | |
| 177 | +/** | |
| 178 | + * @enum {string} | |
| 179 | + * @readonly | |
| 180 | + */ | |
| 181 | +export const SWIPE_STATE = { | |
| 182 | + NONE: 'none', | |
| 183 | + SWIPING: 'swiping', | |
| 184 | + EDITING: 'editing', | |
| 185 | +}; | |
| @@ -2799,7 +2799,7 @@ async function loadUntilMesId(mesId) { | ||
| 2799 | 2799 | while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) { |
| 2800 | 2800 | await showMoreMessages(); |
| 2801 | 2801 | await delay(1); |
| 2802 | 2802 | target = $('#chat').find(`.mes[mesid="${mesId}"]`); |
| 2803 | 2803 | |
| 2804 | 2804 | if (target.length) { |
| 2805 | 2805 | break; |
| @@ -57,7 +57,9 @@ import { | ||
| 57 | 57 | hideSwipeButtons, |
| 58 | 58 | deleteMessage, |
| 59 | 59 | refreshSwipeButtons, |
| 60 | + swipe, | |
| 60 | 61 | isSwipingAllowed, |
| 62 | + swipeState, | |
| 61 | 63 | ensureMessageMediaIsArray, |
| 62 | 64 | getMediaDisplay, |
| 63 | 65 | getMediaIndex, |
| @@ -218,10 +220,12 @@ export function getContext() { | ||
| 218 | 220 | swipe: { |
| 219 | 221 | left: swipe_left, |
| 220 | 222 | right: swipe_right, |
| 223 | + to: swipe, | |
| 221 | 224 | show: showSwipeButtons, |
| 222 | 225 | hide: hideSwipeButtons, |
| 223 | 226 | refresh: refreshSwipeButtons, |
| 224 | 227 | isAllowed: () => isSwipingAllowed, |
| 228 | + state: () => swipeState, | |
| 225 | 229 | }, |
| 226 | 230 | variables: { |
| 227 | 231 | local: { |
| @@ -1,3 +1,4 @@ | ||
| 1 | +import { lodash } from '../lib.js'; | |
| 1 | 2 | import { addOneMessage, chat, displayVersion, setSendButtonState, system_avatar, systemUserName } from '../script.js'; |
| 2 | 3 | import { t } from './i18n.js'; |
| 3 | 4 | import { getMessageTimeStamp } from './RossAscends-mods.js'; |
| @@ -30,89 +31,67 @@ export const system_message_types = { | ||
| 30 | 31 | }; |
| 31 | 32 | |
| 32 | 33 | export async function initSystemMessages() { |
| 33 | 34 | /** @type {Record<string, ChatMessage>} */ |
| 34 | 35 | const resultdefaultMessage = { |
| 35 | - help: { | |
| 36 | 36 | name: systemUserName, |
| 37 | 37 | force_avatar: system_avatar, |
| 38 | 38 | is_user: false, |
| 39 | 39 | is_system: true, |
| 40 | + extra: { swipeable: false }, | |
| 41 | + }; | |
| 42 | + /** @type {Record<string, ChatMessage>} */ | |
| 43 | + const result = { | |
| 44 | + /** @type {ChatMessage} */ | |
| 45 | + help: lodash.merge(structuredClone(defaultMessage), { | |
| 40 | 46 | mes: await renderTemplateAsync('help'), |
| 41 | 47 | }), |
| 42 | - slash_commands: { | |
| 48 | + /** @type {ChatMessage} */ | |
| 43 | - name: systemUserName, | |
| 49 | + slash_commands: lodash.merge(structuredClone(defaultMessage), { | |
| 44 | - force_avatar: system_avatar, | |
| 45 | - is_user: false, | |
| 46 | - is_system: true, | |
| 47 | 50 | mes: '', |
| 48 | 51 | }), |
| 49 | - hotkeys: { | |
| 52 | + /** @type {ChatMessage} */ | |
| 50 | - name: systemUserName, | |
| 53 | + hotkeys: lodash.merge(structuredClone(defaultMessage), { | |
| 51 | - force_avatar: system_avatar, | |
| 52 | - is_user: false, | |
| 53 | - is_system: true, | |
| 54 | 54 | mes: await renderTemplateAsync('hotkeys'), |
| 55 | 55 | }), |
| 56 | - formatting: { | |
| 56 | + /** @type {ChatMessage} */ | |
| 57 | - name: systemUserName, | |
| 57 | + formatting: lodash.merge(structuredClone(defaultMessage), { | |
| 58 | - force_avatar: system_avatar, | |
| 59 | - is_user: false, | |
| 60 | - is_system: true, | |
| 61 | 58 | mes: await renderTemplateAsync('formatting'), |
| 62 | 59 | }), |
| 63 | - macros: { | |
| 60 | + /** @type {ChatMessage} */ | |
| 64 | - name: systemUserName, | |
| 61 | + macros: lodash.merge(structuredClone(defaultMessage), { | |
| 65 | - force_avatar: system_avatar, | |
| 66 | - is_user: false, | |
| 67 | - is_system: true, | |
| 68 | 62 | mes: await renderTemplateAsync('macros'), |
| 69 | 63 | }), |
| 70 | - welcome: { | |
| 64 | + /** @type {ChatMessage} */ | |
| 71 | - name: systemUserName, | |
| 65 | + welcome: lodash.merge(structuredClone(defaultMessage), { | |
| 72 | - force_avatar: system_avatar, | |
| 73 | - is_user: false, | |
| 74 | - is_system: true, | |
| 75 | 66 | mes: await renderTemplateAsync('welcome', { displayVersion }), |
| 76 | 67 | extra: { |
| 77 | 68 | uses_system_ui: true, |
| 78 | 69 | }, |
| 79 | 70 | }), |
| 80 | - empty: { | |
| 71 | + /** @type {ChatMessage} */ | |
| 81 | - name: systemUserName, | |
| 72 | + empty: lodash.merge(structuredClone(defaultMessage), { | |
| 82 | - force_avatar: system_avatar, | |
| 83 | - is_user: false, | |
| 84 | - is_system: true, | |
| 85 | 73 | mes: 'No one hears you. <b>Hint:</b> add more members to the group!', |
| 86 | 74 | }), |
| 87 | - generic: { | |
| 75 | + /** @type {ChatMessage} */ | |
| 88 | - name: systemUserName, | |
| 76 | + generic: lodash.merge(structuredClone(defaultMessage), { | |
| 89 | - force_avatar: system_avatar, | |
| 90 | - is_user: false, | |
| 91 | - is_system: true, | |
| 92 | 77 | mes: 'Generic system message. User `text` parameter to override the contents', |
| 93 | 78 | }), |
| 94 | - welcome_prompt: { | |
| 79 | + /** @type {ChatMessage} */ | |
| 95 | - name: systemUserName, | |
| 80 | + welcome_prompt: lodash.merge(structuredClone(defaultMessage), { | |
| 96 | - force_avatar: system_avatar, | |
| 97 | - is_user: false, | |
| 98 | - is_system: true, | |
| 99 | 81 | mes: await renderTemplateAsync('welcomePrompt'), |
| 100 | 82 | extra: { |
| 101 | 83 | uses_system_ui: true, |
| 102 | 84 | isSmallSys: true, |
| 103 | 85 | }, |
| 104 | 86 | }), |
| 105 | - assistant_note: { | |
| 87 | + /** @type {ChatMessage} */ | |
| 106 | - name: systemUserName, | |
| 88 | + assistant_note: lodash.merge(structuredClone(defaultMessage), { | |
| 107 | - force_avatar: system_avatar, | |
| 108 | - is_user: false, | |
| 109 | - is_system: true, | |
| 110 | 89 | mes: await renderTemplateAsync('assistantNote'), |
| 111 | 90 | extra: { |
| 112 | 91 | uses_system_ui: true, |
| 113 | 92 | isSmallSys: true, |
| 114 | 93 | }, |
| 115 | 94 | }), |
| 116 | 95 | }; |
| 117 | 96 | |
| 118 | 97 | Object.assign(system_messages, result); |
| @@ -132,6 +111,8 @@ export async function initSystemMessages() { | ||
| 132 | 111 | |
| 133 | 112 | /** |
| 134 | 113 | * Gets a system message by type. |
| 114 | + * By default system messages are not swipeable. | |
| 115 | + * This can be overridden by setting extra.swipeable to true. | |
| 135 | 116 | * @param {string} type Type of system message |
| 136 | 117 | * @param {string} [text] Text to be sent |
| 137 | 118 | * @param {ChatMessageExtra} [extra] Additional data to be added to the message |
| @@ -154,10 +135,6 @@ export function getSystemMessageByType(type, text, extra = {}) { | ||
| 154 | 135 | newMessage.mes = getSlashCommandsHelp(); |
| 155 | 136 | } |
| 156 | 137 | |
| 157 | - if (!newMessage.extra || typeof newMessage.extra !== 'object') { | |
| 158 | - newMessage.extra = {}; | |
| 159 | - } | |
| 160 | - | |
| 161 | 138 | newMessage.extra = Object.assign(newMessage.extra, extra); |
| 162 | 139 | newMessage.extra.type = type; |
| 163 | 140 | return newMessage; |
| @@ -2842,4 +2842,45 @@ export async function importFromExternalUrl(url, { preserveFileName = null } = { | ||
| 2842 | 2842 | } |
| 2843 | 2843 | } |
| 2844 | 2844 | |
| 2845 | +/** | |
| 2846 | + * If value is less than min, it's set to min. | |
| 2847 | + * If value is greater than max, it's set to max. | |
| 2848 | + * @param {number} value The target value. | |
| 2849 | + * @param {number} min The minimum for value. | |
| 2850 | + * @param {number} max The maximum for value. | |
| 2851 | + * @returns {number} The clamped value. | |
| 2852 | + */ | |
| 2845 | 2853 | export const clamp = (value, min, max) => Math.min(Math.max(value, min), max); |
| 2854 | + | |
| 2855 | +/** | |
| 2856 | + * Shakes the targetElement. | |
| 2857 | + * @param {HTMLElement|JQuery<HTMLElement>} targetElement | |
| 2858 | + * @param {number} distance Distance in pixels. | |
| 2859 | + * @param {number} duration Duration in milliseconds. | |
| 2860 | + * @param {string} easing CSS easing function. | |
| 2861 | + */ | |
| 2862 | +export function shakeElement(targetElement, distance = 10, duration = 100, easing = 'ease-in-out') { | |
| 2863 | + // Don't call the JQuery animation. | |
| 2864 | + // https://developer.mozilla.org/en-US/docs/Web/API/Element/animate | |
| 2865 | + if (targetElement instanceof jQuery) targetElement = targetElement[0]; | |
| 2866 | + | |
| 2867 | + return targetElement.animate([ | |
| 2868 | + { transform: 'translateX(0)' }, | |
| 2869 | + { transform: `translateX(${distance}px)` }, | |
| 2870 | + { transform: 'translateX(0)' }, | |
| 2871 | + ], { duration, easing }); | |
| 2872 | +} | |
| 2873 | + | |
| 2874 | +/** | |
| 2875 | + * Creates a promise that rejects after a specified delay. | |
| 2876 | + * Used for Promise.race fallbacks. | |
| 2877 | + * @param {number} ms The delay in milliseconds. | |
| 2878 | + * @param {string?} [errorMessage=''] | |
| 2879 | + * @returns {Promise<never>} A promise that rejects. | |
| 2880 | + */ | |
| 2881 | +export function createTimeout(ms, errorMessage = '') { | |
| 2882 | + errorMessage ??= `Operation timed out after ${ms}ms.`; | |
| 2883 | + return new Promise((_, reject) => { | |
| 2884 | + setTimeout(() => reject(new Error(errorMessage)), ms); | |
| 2885 | + }); | |
| 2886 | +} | |
| @@ -135,6 +135,7 @@ function sendAssistantMessage() { | ||
| 135 | 135 | send_date: getMessageTimeStamp(), |
| 136 | 136 | extra: { |
| 137 | 137 | type: system_message_types.ASSISTANT_MESSAGE, |
| 138 | + swipeable: false, | |
| 138 | 139 | }, |
| 139 | 140 | }; |
| 140 | 141 | |
| @@ -677,11 +677,6 @@ small { | ||
| 677 | 677 | margin: 0; |
| 678 | 678 | } |
| 679 | 679 | |
| 680 | -.mes.smallSysMes .swipe_right, | |
| 681 | -.mes.smallSysMes .swipe_left { | |
| 682 | - display: none !important; | |
| 683 | -} | |
| 684 | - | |
| 685 | 680 | .mes.smallSysMes .mes_text { |
| 686 | 681 | padding: 0 !important; |
| 687 | 682 | text-align: center; |
| @@ -1230,6 +1225,14 @@ body .panelControlBar { | ||
| 1230 | 1225 | --swipeCounterMargin: 5px; |
| 1231 | 1226 | } |
| 1232 | 1227 | |
| 1228 | +/* https://stackoverflow.com/a/31819499 */ | |
| 1229 | +/* This has much better performance than .transform on large chats*/ | |
| 1230 | +.slide { | |
| 1231 | + animation: slide calc(var(--slide-mes-x-duration, var(--animation-duration, 125ms))) ease-in-out forwards; | |
| 1232 | + -webkit-animation: slide calc(var(--slide-mes-x-duration, var(--animation-duration, 125ms))) ease-in-out forwards; | |
| 1233 | + will-change: transform; | |
| 1234 | +} | |
| 1235 | + | |
| 1233 | 1236 | .swipe_right, |
| 1234 | 1237 | .swipe_left { |
| 1235 | 1238 | width: 25px; |
| @@ -1257,6 +1260,7 @@ body .panelControlBar { | ||
| 1257 | 1260 | } |
| 1258 | 1261 | |
| 1259 | 1262 | .swipes-counter { |
| 1263 | + opacity: 0.3; | |
| 1260 | 1264 | color: var(--SmartThemeBodyColor); |
| 1261 | 1265 | font-size: 12px; |
| 1262 | 1266 | padding: 0 5px; |
| @@ -1270,14 +1274,11 @@ body .panelControlBar { | ||
| 1270 | 1274 | height: var(--swipeCounterHeight); |
| 1271 | 1275 | } |
| 1272 | 1276 | |
| 1273 | -body:not(.swipeAllMessages) .mes:not(.last_mes) .swipes-counter { | |
| 1274 | - visibility: hidden; | |
| 1275 | -} | |
| 1276 | 1277 | |
| 1277 | 1278 | body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { |
| 1278 | 1279 | /* Avoid expensive DOM queries */ |
| 1279 | 1280 | opacity: 0.3 !important; |
| 1280 | 1281 | display: flex !important; |
| 1281 | 1282 | } |
| 1282 | 1283 | |
| 1283 | 1284 | .swipe_left { |
| @@ -1288,6 +1289,49 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { | ||
| 1288 | 1289 | .swipe_right { |
| 1289 | 1290 | right: 5px; |
| 1290 | 1291 | align-self: center; |
| 1292 | + display: flex; | |
| 1293 | +} | |
| 1294 | + | |
| 1295 | +/* The last swipe must be highlighted */ | |
| 1296 | +.last_swipe :is(.swipes-counter, .swipe_right) { | |
| 1297 | + opacity: 0.7; | |
| 1298 | + /* Doubled opacity needs a longer transition to match a non active arrow. */ | |
| 1299 | + /* (Duration * Active Opacity / Inactive Opacity * 2) */ | |
| 1300 | + transition-duration: calc(var(--animation-duration-2x) * 0.7/0.3); | |
| 1301 | +} | |
| 1302 | + | |
| 1303 | +.fade :is(.swipes-counter, .swipe_left, .swipe_right){ | |
| 1304 | + /* Fade in and out: */ | |
| 1305 | + /* https://nerdy.dev/using-starting-style-and-transition-behavior-for-enter-and-exit-stage-effects */ | |
| 1306 | + /* https://developer.mozilla.org/en-US/docs/Web/CSS/transition-behavior#description */ | |
| 1307 | + transition: opacity calc(var(--animation-duration) * 2) ease-in; | |
| 1308 | + | |
| 1309 | + /* Fade in. */ | |
| 1310 | + @supports (transition-behavior: allow-discrete) { | |
| 1311 | + transition: | |
| 1312 | + opacity var(--animation-duration-2x) ease-in, | |
| 1313 | + display var(--animation-duration-2x) ease-in, | |
| 1314 | + visibility var(--animation-duration-2x) ease-in; | |
| 1315 | + transition-behavior: allow-discrete; | |
| 1316 | + } | |
| 1317 | +} | |
| 1318 | + | |
| 1319 | +/* Hide all but the last message. */ | |
| 1320 | +body:not(.swipeAllMessages) .mes:not(.last_mes) :is(.swipe_left, .swipe_right, .swipes-counter), | |
| 1321 | +/* Hide non-visible swipe_left. */ | |
| 1322 | +.mes:not(.swipes_visible) .swipe_left, | |
| 1323 | +/* Only hide swipe_right if it's not visible, and not the last swipe. */ | |
| 1324 | +.mes:not(.swipes_visible, .last_swipe) :is(.swipe_right), | |
| 1325 | +/* Hide all swipe buttons if .hideAllSwipeButtons. */ | |
| 1326 | +body.hideAllSwipeButtons :is(.swipe_left, .swipe_right), | |
| 1327 | +.swipes-counter[hidden], | |
| 1328 | +.swipe_right[hidden], | |
| 1329 | +.swipe_left[hidden] { | |
| 1330 | + /* Fade out */ | |
| 1331 | + opacity: 0 !important; | |
| 1332 | + /* With display:none, the swipes-counter div wil collapse, shifting the chevron. */ | |
| 1333 | + visibility: hidden; | |
| 1334 | + /* Interactivity has limited availability https://caniuse.com/?search=interactivity */ | |
| 1291 | 1335 | } |
| 1292 | 1336 | |
| 1293 | 1337 | .ui-settings { |
| @@ -4445,11 +4489,12 @@ input[type="range"]::-webkit-slider-thumb { | ||
| 4445 | 4489 | field-sizing: content; |
| 4446 | 4490 | } |
| 4447 | 4491 | |
| 4448 | 4492 | body:is([data-generating="true"] #send_but, [data-swiping="true"]) :is( |
| 4449 | -body[data-generating="true"] #mes_continue, | |
| 4493 | + #send_but, | |
| 4450 | -body[data-generating="true"] #mes_impersonate, | |
| 4494 | + #mes_continue, | |
| 4451 | -body[data-generating="true"] #chat .last_mes .mes_buttons, | |
| 4495 | + #mes_impersonate, | |
| 4452 | -body[data-generating="true"] #chat .last_mes .mes_reasoning_actions { | |
| 4496 | + #chat .last_mes .mes_buttons, | |
| 4497 | + #chat .last_mes .mes_reasoning_actions) { | |
| 4453 | 4498 | display: none; |
| 4454 | 4499 | } |
| 4455 | 4500 | |