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>

fc85b205ac3b65eac3f1ed0f70d6b37f057d5f95

DeclineThyself <235079501+DeclineThyself@users.noreply.github.com>

Signed
12 files changed, +784 -320Ignore whitespace
public/css/animations.css+20 -0
@@ -132,3 +132,23 @@
132132 overflow-y: hidden;
133133 }
134134}
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+}
public/global.d.ts+6 -0
@@ -6,6 +6,7 @@ import { oai_settings } from './scripts/openai';
66import { textgenerationwebui_settings } from './scripts/textgen-settings';
77import { FileAttachment } from './scripts/chats';
88import { ReasoningMessageExtra } from './scripts/reasoning';
9+import { OVERSWIPE_BEHAVIOR } from './scripts/constants';
910
1011declare global {
1112 // Custom types
@@ -80,6 +81,9 @@ declare global {
8081 title?: string;
8182 isSmallSys?: boolean;
8283 token_count?: number;
84+ /** When false, the message cannot be swiped. */
85+ swipeable?: boolean;
86+ overswipe_behavior?: OVERSWIPE_BEHAVIOR;
8387 files?: FileAttachment[];
8488 inline_image?: boolean;
8589 media_display?: string;
@@ -202,4 +206,6 @@ declare global {
202206 rgba: string;
203207 }
204208 };
209+
210+ type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;
205211}
public/index.html+2 -2
@@ -7052,7 +7052,7 @@
70527052 <div class="mes_timer"></div>
70537053 <div class="tokenCounterDisplay"></div>
70547054 </div>
70557055 <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div>
70567056 <div class="mes_block">
70577057 <div class="ch_name flex-container justifySpaceBetween">
70587058 <div class="flex-container flex1 alignitemscenter">
@@ -7116,7 +7116,7 @@
71167116 <div class="mes_bias"></div>
71177117 </div>
71187118 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
71197119 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>
71207120 <div class="swipes-counter"></div>
71217121 </div>
71227122 </div>
public/script.js+565 -232
@@ -181,8 +181,10 @@ import {
181181 canUseNegativeLookbehind,
182182 trimSpaces,
183183 clamp,
184+ shakeElement,
185+ createTimeout,
184186} from './scripts/utils.js';
185187import { 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';
186188
187189import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';
188190import { 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;
372374export let name2 = systemUserName;
373375/** @type {ChatMessage[]} */
374376export 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;
376382let chatSaveTimeout;
377383let importFlashTimeout;
378384export let isChatSaving = false;
@@ -561,6 +567,7 @@ let chat_file_for_del = '';
561567export let online_status = 'no_connection';
562568
563569export let is_send_press = false; //Send generation
570+export const isGenerating = () => (is_send_press || is_group_generating);
564571
565572let this_del_mes = -1;
566573
@@ -574,7 +581,14 @@ export let settings;
574581export let amount_gen = 80; //default max length of AI generated responses
575582export let max_context = 2048;
576583
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+
578592export let extension_prompts = {};
579593
580594export let main_api;// = "kobold";
@@ -1376,10 +1390,11 @@ export async function showMoreMessages(messagesToLoad = null) {
13761390
13771391 while (messageId > 0 && count > 0) {
13781392 let newMessageId = messageId - 1;
13791393 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false });
13801394 count--;
13811395 messageId--;
13821396 }
1397+ refreshSwipeButtons();
13831398
13841399 if (messageId == 0) {
13851400 $('#show_more_messages').remove();
@@ -1410,7 +1425,7 @@ export async function printMessages() {
14101425
14111426 chatElement.find('.mes').removeClass('last_mes');
14121427 chatElement.find('.mes').last().addClass('last_mes');
14131428 refreshSwipeButtons(false, false);
14141429 applyStylePins();
14151430 scrollChatToBottom({ waitForFrame: true });
14161431 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
@@ -1471,6 +1486,7 @@ export async function clearChat() {
14711486 if (is_delete_mode) {
14721487 $('#dialogue_del_mes_cancel').trigger('click');
14731488 }
1489+ //This will also remove non '.mes' elements, e.g. '<div id="show_more_messages">Show more messages</div>'.
14741490 chatElement.children().remove();
14751491 if ($('.zoomed_avatar[forChar]').length) {
14761492 console.debug('saw avatars to remove');
@@ -1575,9 +1591,17 @@ export async function reloadCurrentChat() {
15751591 * Send the message currently typed into the chat box.
15761592 */
15771593export 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.
15781600 if (is_send_press) return;
15791601 if (isExecutingCommandsFromChatInput) return;
15801602
1603+ hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible.
1604+
15811605 let generateType = 'normal';
15821606 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
15831607 // message was sent from a character (not the user or the system).
@@ -1597,7 +1621,9 @@ export async function sendTextareaMessage() {
15971621 await newAssistantChat({ temporary: false });
15981622 }
15991623
16001624 returnlet generation = await Generate(generateType);
1625+ showSwipeButtons();
1626+ return generation;
16011627}
16021628
16031629/**
@@ -2362,7 +2388,7 @@ export function addCopyToCodeBlocks(messageElement) {
23622388 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
23632389 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
23642390 * @param {number} [options.forceId=null] Force the message ID
23652391 * @param {boolean} [options.showSwipes=true] Whether to showrefresh the swipe buttons.
23662392 * @returns {void}
23672393 */
23682394export 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
24862512 });
24872513
24882514 if (type === 'swipe') {
2489- const messageId = forceId ?? chat.length - 1;
2515+ const swipeMessage = chatElement.find(`[mesid="${newMessageId}"]`);
2490- const swipeMessage = chatElement.find(`[mesid="${messageId}"]`);
24912516 swipeMessage.attr('swipeid', params.swipeId);
24922517 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
24932518 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
25052530 swipeMessage.find('.tokenCounterDisplay').empty();
25062531 }
25072532 } 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);
25102534 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2511- showSwipes && hideSwipeButtons();
25122535 }
25132536
25142537 addCopyToCodeBlocks(newMessage);
25152538
25162539 // 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));
25212542 }
25222543
2544+ //last_mes should always be updated.
2545+ chatElement.find('.mes').removeClass('last_mes');
2546+ chatElement.find('.mes').last().addClass('last_mes');
25232547 if (showSwipes) {
2524- chatElement.find('.mes').last().addClass('last_mes');
2525- chatElement.find('.mes').eq(-2).removeClass('last_mes');
25262548 refreshSwipeButtons();
25272549 }
25282550
@@ -3275,7 +3297,7 @@ class StreamingProcessor {
32753297 }
32763298
32773299 markUIGenStopped() {
32783300 activateSendButtonsunblockGeneration();
32793301 }
32803302
32813303 async onStartStreaming(text) {
@@ -3404,14 +3426,14 @@ class StreamingProcessor {
34043426 }
34053427
34063428 async onFinishStreaming(messageId, text) {
3407- this.markUIGenStopped();
34083429 await this.onProgressStreaming(messageId, text, true);
34093430 addCopyToCodeBlocks(const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`));
3431+ const message = chat[messageId];
3432+ addCopyToCodeBlocks(messageElement);
34103433
34113434 await this.reasoningHandler.finish(messageId);
34123435
34133436 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3414- const message = chat[messageId];
34153437 const swipeInfoExtra = structuredClone(message.extra ?? {});
34163438 delete swipeInfoExtra.token_count;
34173439 delete swipeInfoExtra.reasoning;
@@ -3424,15 +3446,20 @@ class StreamingProcessor {
34243446 };
34253447 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));
34263448 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);
34273449 chat[messageId]message.swipes.push(...this.swipes);
34283450 chat[messageId]message.swipe_info.push(...swipeInfoArray);
34293451 }
34303452
3453+ syncMesToSwipe(messageId);
3454+ saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3455+
34313456 if (Array.isArray(this.images) && this.images.length > 0) {
34323457 await processImageAttachment(chat[messageId]message, { imageUrls: this.images });
34333458 appendMediaToMessage(chat[messageId]message, $(this.messageDom));
34343459 }
34353460
3461+ this.markUIGenStopped();
3462+
34363463 if (this.type !== 'impersonate') {
34373464 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
34383465 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
@@ -3440,15 +3467,13 @@ class StreamingProcessor {
34403467 await eventSource.emit(event_types.IMPERSONATE_READY, text);
34413468 }
34423469
3443- syncMesToSwipe(messageId);
3470+ updateSwipeCounter(messageId, { message, messageElement });
3444- saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3445- await saveChatConditional();
3446- unblockGeneration();
34473471
34483472 const isAborted = this.abortController.signal.aborted;
34493473 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 });
34513475 }
3476+ saveChatDebounced();
34523477
34533478 playMessageSound();
34543479 }
@@ -3458,7 +3483,6 @@ class StreamingProcessor {
34583483 this.isStopped = true;
34593484
34603485 this.markUIGenStopped();
3461- unblockGeneration();
34623486
34633487 const noEmitTypes = ['swipe', 'impersonate', 'continue'];
34643488 if (!noEmitTypes.includes(this.type)) {
@@ -5171,7 +5195,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
51715195 const isAborted = abortController && abortController.signal.aborted;
51725196 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
51735197 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+
51755200 }
51765201
51775202 console.debug('/api/chats/save called by /Generate');
@@ -5303,7 +5328,6 @@ function unblockGeneration(type) {
53035328
53045329 is_send_press = false;
53055330 activateSendButtons();
5306- showSwipeButtons();
53075331 setGenerationProgress(0);
53085332 flushEphemeralStoppingStrings();
53095333 flushWIInjections();
@@ -6358,6 +6382,47 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
63586382}
63596383
63606384/**
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+/**
63616426 * 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).
63626427 *
63636428 * If the swipe data is invalid in some way, this function will exit out without doing anything.
@@ -6539,6 +6604,7 @@ function getGeneratingModel(mes) {
65396604export function activateSendButtons() {
65406605 is_send_press = false;
65416606 hideStopButton();
6607+ showSwipeButtons();
65426608 delete document.body.dataset.generating;
65436609}
65446610
@@ -6547,6 +6613,7 @@ export function activateSendButtons() {
65476613 */
65486614export function deactivateSendButtons() {
65496615 showStopButton();
6616+ hideSwipeButtons();
65506617 document.body.dataset.generating = 'true';
65516618}
65526619
@@ -7583,8 +7650,11 @@ function updateMessage(div) {
75837650 const mesElement = div.closest('.mes');
75847651 const mes = chat[mesElement.attr('mesid')];
75857652
7653+ // editing old messages
7654+ mes['extra'] ??= {};
7655+
75867656 let regexPlacement;
75877657 if (mes?.is_user) {
75887658 regexPlacement = regex_placement.USER_INPUT;
75897659 } else if (mes.extra?.type === 'narrator') {
75907660 regexPlacement = regex_placement.SLASH_COMMAND;
@@ -7614,15 +7684,11 @@ function updateMessage(div) {
76147684 }
76157685 mes['mes'] = text;
76167686 if (mes['swipe_id'] !== undefined) {
7687+ ensureSwipes(mes);
76177688 mes['swipes'][mes['swipe_id']] = text;
76187689 }
76197690
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) {
76267692 mes.extra.bias = bias ?? null;
76277693 } else {
76287694 mes.extra.bias = null;
@@ -7693,8 +7759,7 @@ export async function messageEdit(editMessageId) {
76937759 this_edit_mes_id = editMessageId;
76947760 this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2);
76957761
7696- const hideCounters = editMessageId < chat.length - 1;
7762+ refreshSwipeButtons();
7697- hideSwipeButtons({ hideCounters });
76987763
76997764 const chatScrollPosition = chatElement.scrollTop();
77007765 const messageBlock = messageElement.find('.mes_block');
@@ -7749,7 +7814,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) {
77497814 if (this?.classList?.contains('mes_edit_cancel')) {
77507815 thisMesDiv = $(this).closest('.mes');
77517816 } else {
77527817 thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`);
77537818 }
77547819
77557820 const thisMesBlock = thisMesDiv.find('.mes_block');
@@ -7830,11 +7895,17 @@ async function messageEditMove(sourceId, targetId) {
78307895 }
78317896
78327897 updateViewMessageIds();
7898+ refreshSwipeButtons();
78337899 await saveChatConditional();
78347900 return true;
78357901}
78367902
78377903async 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+
78387909 let { mesBlock, text, mes, bias } = updateMessage(div);
78397910 if (this_edit_mes_id == 0) {
78407911 text = substituteParams(text);
@@ -8559,99 +8630,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
85598630
85608631/**
85618632 * Update the swipe counter for mesId.
8633+ * By default, the swipe counter's opacity will appear greyed out. The opacity is changed with CSS.
85628634 * @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.
85638638 */
85648639export 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];
85668641 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);
85698651}
85708652
85718653/**
85728654 * Swipe buttons areReturns oftentrue toggledif tomessages updateare theirgenerally positionswipeable.
8573- * This should be replaced with a more efficient function.
8655+ * @returns {boolean}
85748656 */
85758657export 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+ );
85788668}
85798669
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];
85828680
8583- if (chat.length === 0) {
8681+ //If the message does not have swipes, create them.
8584- return;
8682+ if (ensureSwipes(message)) {
8683+ syncMesToSwipe(messageId);
85858684 }
85868685
85878686 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.
8595- }
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; }
8705+}
85968706
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');
85998750 return;
8751+ //Don't hide all swipe buttons.
8752+ } else {
8753+ //CSS will hide all messages.
8754+ $('body').removeClass('hideAllSwipeButtons');
86008755 }
8756+ //Non-messages can appear in chat. '.mes' is required.
8757+ const messageElements = chatElement.children('.mes[mesid]');
86018758
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- }
86178760
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');
86248766
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.
8632- }
8774+ const isLastSwipe = (message?.swipes?.length ?? 1) - 1 <= (message?.swipe_id ?? 0);
8633- if ((chat[mesId].swipes.length - swipeId) === 1) {
8775+ const hasSwipes = (message?.swipes?.length > 1);
8634- //chevron was moved out of hardcode in HTML to class toggle dependent on last_mes or not
8776+ const overswipe = getOverswipeBehavior(messageId, message);
8635- //necessary for 'swipe_right' div in past messages to have no chevron if 'show swipes for all messages' is turned on
8777+
8636- swipeRight.css('opacity', '0.7');
8778+ // Chevrons should always be shown on pristine greetings: https://github.com/SillyTavern/SillyTavern/pull/4712#issuecomment-3557893373
8637- swipeCounter.css('opacity', '0.7');
8779+ const pristineGreeting = overswipe == OVERSWIPE_BEHAVIOR.PRISTINE_GREETING;
8638- }
86398780
8640- //allows for writing individual swipe counters for past messages
8781+ //The swipe button will be shown if an overswipe would trigger REGENERATE or EDIT_GENERATE.
8641- const lastSwipeCounter = $('.last_mes .swipes-counter');
8782+ const isOverswipeable = isLastSwipe &&
8642- lastSwipeCounter.text(swipeCounterText).show();
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');
8796+ }
8797+ });
8798+}
8799+/**
8800+ * This function is misleadingly named. It allows generation then refreshes the swipe buttons and counters.
8801+ */
8802+export function showSwipeButtons() {
8803+ swipesHidden = false;
8804+ refreshSwipeButtons();
86438805}
86448806
86458807/**
8808+ * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters.
86468809 * @param {object} [options] Options
86478810 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.
86488811 */
86498812export function hideSwipeButtons({ hideCounters = false } = {}) {
86508813 isSwipingAllowedswipesHidden = falsetrue;
8651- chatElement.find('.swipe_right').hide();
8814+ refreshSwipeButtons();
8652- chatElement.find('.swipe_left').hide();
8815+
86538816 if (hideCounters === true) {
86548817 chatElement.find('.last_mes .swipes-counter').hideprop('hidden', true);
86558818 }
86568819}
86578820
@@ -8694,14 +8857,17 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
86948857
86958858 // Select the next swipe, or the one before if it was the last one
86968859 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);
8697- syncSwipeToMes(messageId, newSwipeId);
86988860
86998861 chat_metadata['tainted'] = true;
87008862
8863+ messageId = Number(messageId);
8864+ swipeId = Number(swipeId);
87018865 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 });
87028869
87038870 await saveChatConditional();
8704- await reloadCurrentChat();
87058871
87068872 return newSwipeId;
87078873}
@@ -8791,7 +8957,7 @@ export function updateEditArrowClasses() {
87918957 return;
87928958 }
87938959
87948960 const message = chatElement.findchildren('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`);
87958961
87968962 const downButton = message.find('.mes_edit_down');
87978963 const upButton = message.find('.mes_edit_up');
@@ -9226,6 +9392,23 @@ export async function createOrEditCharacter(e) {
92269392}
92279393
92289394/**
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+/**
92299412 * Formats a counter for a swipe view.
92309413 * @param {number} current The current number of items.
92319414 * @param {number} total The total number of items.
@@ -9240,41 +9423,61 @@ function formatSwipeCounter(current, total) {
92409423
92419424/**
92429425 * Handles the swipe event.
92439426 * @param {JQuery.EventSwipeEvent} _eventevent Event.
92449427 * @param {'left'|'right'} direction The direction to swipe.
92459428 * @param {object} params Additional parameters.
92469429 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
92479430 * @param {boolean} [params.repeated] Is the swipe event repeated.
92489431 * @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.
92499435 */
92509436export async function swipe(_eventevent, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) {
92519437 if (chat.length === 0) {
92529438 console.warn('Swipe was called on an empty chat.');
92539439 return;
92549440 }
92559441
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;
92649442 let messageIndex;
92659443
92669444 //Only set messageIndex if message exists because -1 is truthy.
92679445 if (message) {
92689446 messageIndex = chat.indexOf(message);
92699447 if (messageIndex === -1 && typeof (forceMesId) != 'number') {
92709448 console.error(`The message must exist in chat. ${message};`);
92719449 return;
92729450 }
92739451 }
92749452
92759453 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;
92769479
92779480 const thisMesDiv = chatElement.children('.mes').filter(`.mes[mesid="${mesId}"]`);
92789481 const thisMesText = thisMesDiv.find('.mes_block .mes_text');
92799482 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;
92809483 const thisMesTextHeight = thisMesText[0]?.scrollHeight;
@@ -9283,43 +9486,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha
92839486 return;
92849487 }
92859488 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);
92869489 let newSwipeId = Number(forceSwipeId ?? originalSwipeId);
92879490
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+ }
9510+
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;
92919516
9292- async function endSwipe() {
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) {
92939522 //Wait for the generation to end.
92949523 try {
9295- await generation;
9524+ //`mes_buttons` need to be hidden until the animation completes.
9525+ if (generation) {
9526+ document.body.dataset.swiping = 'true';
9527+ await generation;
9528+ }
92969529 }
92979530 catch (error) {
92989531 console.warn(`Swipe failed, Swiping back. ${error}`);
92999532 }
9300- //Allow for another swipe.
9301- showSwipeButtons();
93029533
93039534 //Clamp Id between swipes.
93049535 let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1));
93059536
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-
93139537 await updateSwipeCounter(mesId);
93149538 //Fallback.
93159539 if (mesId != chat.length - 1) {
93169540 await updateSwipeCounter(chat.length - 1);
93179541 }
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();
9580+ }
9581+
9582+ //Allow for another swipe.
9583+ swipeState = SWIPE_STATE.NONE;
9584+ delete document.body.dataset.swiping;
9585+ showSwipeButtons();
93189586 }
93199587
93209588 async function standardSwipe(newSwipeId) {
93219589 //If swipe_id has changed, or the source is being deleted.
93229590 if (newSwipeId !== originalSwipeId || source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
93239591 //Update the chat.
93249592 await loadFromSwipeId(mesId, newSwipeId);
93259593 //Transition to the new chat.
@@ -9329,6 +9597,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha
93299597 }
93309598
93319599 /**
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+ /**
93329621 * Sets the message to the newSwipeId and loads it.
93339622 * @param {number} mesId
93349623 * @param {number} newSwipeId
@@ -9337,55 +9626,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha
93379626 //Update the swipe_id.
93389627 chat[mesId]['swipe_id'] = newSwipeId;
93399628
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+
93479636 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;
93519640 }
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- };
93679641
93689642 /**
93699643 * Animates a swipe for all messages >= mesId.
93709644 * @param {number} mesId
93719645 * @param {numberobject} xparams
93729646 * @param {numberstring} duration[params.xStart='opx']
9647+ * @param {string} [params.xEnd='0px']
9648+ * @param {number} [params.duration=animation_duration]
9649+ * @param {string} [params.classes=''] Additional CSS classes to target during the swipe.
9650+ * @param {boolean} [params.freeze=true] When true, do not remove the class from the animation, leaving it stuck at xEnd.
9651+ * @returns {Promise<boolean|Function>} endSlide unfreezes the messages from xEnd.
93739652 */
9374- async function animateSwipeTransition(mesId, x, duration) {
9653+ async function animateSwipeTransition(mesId, { xStart = '0px', xEnd = '0px', duration = animation_duration, classes = '', freeze = false } = {}) {
9375- //Selects the swiped message.
9654+ // If the animation_duration is zero, the 'animationend' promise will never resolve.
9376- const swipedMessagesDiv = chatElement.children().filter((index, div) => {
9655+ //Skip the animation if it's faster than 50ms.
9377- const $div = $(div);
9656+ if (duration <= 50) return;
9378- return mesId === Number($div.attr('mesid'));
9379- });
9380- const swipedElementsDiv = swipedMessagesDiv.children('.mes_block, .mesAvatarWrapper');
93819657
9382- //Swipe.
9658+ //Select MAXIMUM_ANIMATED messages after mesId. Ideally, only visible messages would be animated.
9383- await transitionPromise(swipedElementsDiv, {
9659+ const MAXIMUM_ANIMATED = 100;
9384- x: x,
9660+
9385- duration: duration,
9661+ const messages = chatElement.children('.mes');
9386- easing: animation_easing,
9662+ const firstDisplayedMesId = Number(messages.first().attr('mesid'));
9387- queue: false,
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);
93889670 });
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+ }
9705+
9706+ //If not frozen, end the slide now.
9707+ return freeze ? endSlide : endSlide();
9708+ }
9709+ }
9710+ console.warn(`No animatable messages were found after message #${mesId}.`);
9711+ return false;
93899712 }
93909713
93919714 function getMessageBottomHeight(thisMesDiv) {
@@ -9427,11 +9750,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94279750 /**
94289751 * Anime a swipe, optionally running a generation.
94299752 * @param {boolean} run_generate
9753+ * @param {boolean} [skipSwipeOut=false]
94309754 */
94319755 async function animateSwipe(run_generate = false, skipSwipeOut = false) {
9756+
9757+ if (!skipSwipeOut) {
9758+ //Swipe out.
9759+ await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });
9760+ }
94329761
9433- //Swipe out.
9434- await animateSwipeTransition(mesId, swipeRange, swipeDuration);
94359762
94369763 if (run_generate) {
94379764 await updateSwipeCounter(mesId);
@@ -9447,7 +9774,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94479774
94489775 //Only scroll when swiping the last message.
94499776 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 });
94519779
94529780 if (power_user.message_token_count_enabled) {
94539781 if (!chat[mesId].extra) {
@@ -9465,10 +9793,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94659793 thisMesDiv.css('height', thisMesDivHeight);
94669794 expandNewMessage(thisMesDiv);
94679795
9468-
9469- //Jump to the opposite side.
9470- await animateSwipeTransition(mesId, -swipeRange, 0);
9471-
94729796 appendMediaToMessage(chat[mesId], thisMesDiv);
94739797
94749798 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));
@@ -9476,12 +9800,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94769800 if (run_generate && !is_send_press) {
94779801 is_send_press = true;
94789802 generation = Generate('swipe');
9479- } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) {
9480- saveChatDebounced();
94819803 }
94829804
94839805 //Swipe in from the opposite side.
9484- await animateSwipeTransition(mesId, 0, swipeDuration);
9806+ await animateSwipeTransition(mesId, { xStart: `${-swipeRange}px`, xEnd: `${0}px`, duration: swipeDuration });
94859807 }
94869808
94879809 if (mesId === Number(this_edit_mes_id)) {
@@ -9496,7 +9818,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94969818 }
94979819
94989820 //If the swipe is not being deleted.
94999821 if (source != 'delete'SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {
95009822
95019823 // Make sure ad-hoc changes to extras are saved before swiping away
95029824 syncMesToSwipe(mesId);
@@ -9515,19 +9837,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha
95159837 }
95169838 // If the user is holding down the key and we're at the last or first swipe, don't do anything.
95179839 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;
95189840 if (source === 'keyboard'SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {
95199841 await endSwipe();
95209842 return;
95219843 }
95229844 } else if (source == 'delete'SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
95239845 //If the swipe is being deleted or reverted.
95249846 await standardSwipe(newSwipeId);
95259847 return;
95269848 }
95279849
95289850 //If swiping left.
95299851 if (direction === SWIPE_DIRECTION.LEFT) {
9530- newSwipeId--;
9852+ if (forceSwipeId == null) newSwipeId--;
95319853 //Loop to last swipe if negative.
95329854 if (newSwipeId < 0) {
95339855 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);
@@ -9539,13 +9861,13 @@ export async function swipe(_event, direction, { source, repeated, message = cha
95399861 await endSwipe();
95409862 return;
95419863 }
95429864 await standardSwipe(newSwipeId);
95439865 return;
95449866 }
95459867 //If swiping right.
95469868 else if (direction === SWIPE_DIRECTION.RIGHT) {
95479869 // make new slot in array
9548- newSwipeId++;
9870+ if (forceSwipeId == null) newSwipeId++;
95499871
95509872 //Minimum of zero.
95519873 if (newSwipeId < 0) {
@@ -9555,35 +9877,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha
95559877 return;
95569878 }
95579879
9558- //if swipe id of last message is the same as the length of the 'swipes' array and not the greeting.
9880+ //If overswiping.
95599881 if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) {
95609882 newSwipeId = chat[mesId]['swipes'].length;
95619883
95629884 //Update the swipe_id.
95639885 chat[mesId]['swipe_id'] = newSwipeId;
95649886
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) {
95679891 //Cancel swipe.
95689892 chat[mesId]['swipe_id'] = originalSwipeId;
95699893 await endSwipe();
95709894 return;
95719895 } else {
9572- //Generate.
9896+ //Regenerate the message
9573- await loadFromSwipeId(mesId, newSwipeId);
9897+ else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
9898+ clearMessageData(chat[mesId]);
95749899 let run_generate = true;
9900+ //Generate.
95759901 await animateSwipe(run_generate);
95769902 await endSwipe();
95779903 return;
95789904 }
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) {
95839907 newSwipeId = 0;
95849908 }
95859909 }
95869910 await standardSwipe(newSwipeId);
95879911 return;
95889912 }
95899913}
@@ -9591,28 +9915,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha
95919915/**
95929916 * @deprecated Use `swipe` instead.
95939917 * Handles the swipe to the left event.
95949918 * @param {JQuery.EventSwipeEvent} _event[event] Event.
95959919 * @param {object} params Additional parameters.
95969920 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
95979921 * @param {boolean} [params.repeated] Is the swipe event repeated.
95989922 * @param {object} [params.message] The chat message to swipe.
95999923 */
96009924export async function swipe_left(_eventevent, { source, repeated, message } = {}) {
96019925 await swipe.call(this, _eventevent, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });
96029926}
96039927
96049928/**
96059929 * @deprecated Use `swipe` instead.
96069930 * Handles the swipe to the right event.
96079931 * @param {JQuery.EventSwipeEvent} [_eventevent] Event.
96089932 * @param {object} params Additional parameters.
96099933 * @param {stringimport('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
96109934 * @param {boolean} [params.repeated] Is the swipe event repeated.
96119935 * @param {object} [params.message] The chat message to swipe.
96129936 */
96139937//MARK: swipe_right
96149938export async function swipe_right(_eventevent = null, { source, repeated, message } = {}) {
96159939 await swipe.call(this, _eventevent, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });
96169940}
96179941
96189942/**
@@ -10782,10 +11106,12 @@ jQuery(async function () {
1078211106 }
1078311107
1078411108 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+ }
1078611114 if (is_send_press == false) {
10787- //hideSwipeButtons();
10788-
1078911115 if (selected_group) {
1079011116 regenerateGroup();
1079111117 }
@@ -10804,7 +11130,14 @@ jQuery(async function () {
1080411130 }
1080511131
1080611132 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+ }
1080811141
1080911142 if (is_send_press == false || fromSlashCommand) {
1081011143 is_send_press = true;
public/scripts/RossAscends-mods.js+5 -5
@@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js';
3737import { chat_completion_sources, oai_settings } from './openai.js';
3838import { getTokenCountAsync } from './tokenizers.js';
3939import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
4040import { debounce_timeout, SWIPE_SOURCE } from './constants.js';
4141
4242import { Popup } from './popup.js';
4343import { accountStorage } from './util/AccountStorage.js';
@@ -1106,7 +1106,7 @@ export function initRossMods() {
11061106
11071107 if (event.key == 'ArrowLeft') { //swipes left
11081108 if (
11091109 isSwipingAllowed() &&
11101110 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
11111111 $('#send_textarea').val() === '' &&
11121112 $('#character_popup').css('display') === 'none' &&
@@ -1114,13 +1114,13 @@ export function initRossMods() {
11141114 !isInputElementInFocus() &&
11151115 !isModifiedKeyboardEvent(event)
11161116 ) {
11171117 $('.swipe_left:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
11181118 return;
11191119 }
11201120 }
11211121 if (event.key == 'ArrowRight') { //swipes right
11221122 if (
11231123 isSwipingAllowed() &&
11241124 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
11251125 $('#send_textarea').val() === '' &&
11261126 $('#character_popup').css('display') === 'none' &&
@@ -1128,7 +1128,7 @@ export function initRossMods() {
11281128 !isInputElementInFocus() &&
11291129 !isModifiedKeyboardEvent(event)
11301130 ) {
11311131 $('.swipe_right:last').trigger('click', { source: 'keyboard'SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
11321132 return;
11331133 }
11341134 }
public/scripts/constants.js+37 -0
@@ -140,9 +140,46 @@ export const SCROLL_BEHAVIOR = {
140140};
141141
142142/**
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+/**
143160 * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}}
144161 */
145162export const SWIPE_DIRECTION = {
146163 LEFT: 'left',
147164 RIGHT: 'right',
148165};
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+};
public/scripts/power-user.js+1 -1
@@ -2799,7 +2799,7 @@ async function loadUntilMesId(mesId) {
27992799 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
28002800 await showMoreMessages();
28012801 await delay(1);
28022802 target = $('#chat').find(`.mes[mesid="${mesId}"]`);
28032803
28042804 if (target.length) {
28052805 break;
public/scripts/st-context.js+5 -1
@@ -57,7 +57,9 @@ import {
5757 hideSwipeButtons,
5858 deleteMessage,
5959 refreshSwipeButtons,
60+ swipe,
6061 isSwipingAllowed,
62+ swipeState,
6163 ensureMessageMediaIsArray,
6264 getMediaDisplay,
6365 getMediaIndex,
@@ -218,10 +220,12 @@ export function getContext() {
218220 swipe: {
219221 left: swipe_left,
220222 right: swipe_right,
223+ to: swipe,
221224 show: showSwipeButtons,
222225 hide: hideSwipeButtons,
223226 refresh: refreshSwipeButtons,
224227 isAllowed: () => isSwipingAllowed,
228+ state: () => swipeState,
225229 },
226230 variables: {
227231 local: {
public/scripts/system-messages.js+41 -64
@@ -1,3 +1,4 @@
1+import { lodash } from '../lib.js';
12import { addOneMessage, chat, displayVersion, setSendButtonState, system_avatar, systemUserName } from '../script.js';
23import { t } from './i18n.js';
34import { getMessageTimeStamp } from './RossAscends-mods.js';
@@ -30,89 +31,67 @@ export const system_message_types = {
3031};
3132
3233export async function initSystemMessages() {
34+ /** @type {ChatMessage} */
35+ const defaultMessage = {
36+ name: systemUserName,
37+ force_avatar: system_avatar,
38+ is_user: false,
39+ is_system: true,
40+ extra: { swipeable: false },
41+ };
3342 /** @type {Record<string, ChatMessage>} */
3443 const result = {
35- help: {
44+ /** @type {ChatMessage} */
36- name: systemUserName,
45+ help: lodash.merge(structuredClone(defaultMessage), {
37- force_avatar: system_avatar,
38- is_user: false,
39- is_system: true,
4046 mes: await renderTemplateAsync('help'),
4147 }),
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,
4750 mes: '',
4851 }),
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,
5454 mes: await renderTemplateAsync('hotkeys'),
5555 }),
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,
6158 mes: await renderTemplateAsync('formatting'),
6259 }),
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,
6862 mes: await renderTemplateAsync('macros'),
6963 }),
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,
7566 mes: await renderTemplateAsync('welcome', { displayVersion }),
7667 extra: {
7768 uses_system_ui: true,
7869 },
7970 }),
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,
8573 mes: 'No one hears you. <b>Hint&#58;</b> add more members to the group!',
8674 }),
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,
9277 mes: 'Generic system message. User `text` parameter to override the contents',
9378 }),
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,
9981 mes: await renderTemplateAsync('welcomePrompt'),
10082 extra: {
10183 uses_system_ui: true,
10284 isSmallSys: true,
10385 },
10486 }),
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,
11089 mes: await renderTemplateAsync('assistantNote'),
11190 extra: {
11291 uses_system_ui: true,
11392 isSmallSys: true,
11493 },
11594 }),
11695 };
11796
11897 Object.assign(system_messages, result);
@@ -132,6 +111,8 @@ export async function initSystemMessages() {
132111
133112/**
134113 * 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.
135116 * @param {string} type Type of system message
136117 * @param {string} [text] Text to be sent
137118 * @param {ChatMessageExtra} [extra] Additional data to be added to the message
@@ -154,10 +135,6 @@ export function getSystemMessageByType(type, text, extra = {}) {
154135 newMessage.mes = getSlashCommandsHelp();
155136 }
156137
157- if (!newMessage.extra || typeof newMessage.extra !== 'object') {
158- newMessage.extra = {};
159- }
160-
161138 newMessage.extra = Object.assign(newMessage.extra, extra);
162139 newMessage.extra.type = type;
163140 return newMessage;
public/scripts/utils.js+41 -0
@@ -2842,4 +2842,45 @@ export async function importFromExternalUrl(url, { preserveFileName = null } = {
28422842 }
28432843}
28442844
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+ */
28452853export 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+}
public/scripts/welcome-screen.js+1 -0
@@ -135,6 +135,7 @@ function sendAssistantMessage() {
135135 send_date: getMessageTimeStamp(),
136136 extra: {
137137 type: system_message_types.ASSISTANT_MESSAGE,
138+ swipeable: false,
138139 },
139140 };
140141
public/style.css+60 -15
@@ -677,11 +677,6 @@ small {
677677 margin: 0;
678678}
679679
680-.mes.smallSysMes .swipe_right,
681-.mes.smallSysMes .swipe_left {
682- display: none !important;
683-}
684-
685680.mes.smallSysMes .mes_text {
686681 padding: 0 !important;
687682 text-align: center;
@@ -1230,6 +1225,14 @@ body .panelControlBar {
12301225 --swipeCounterMargin: 5px;
12311226}
12321227
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+
12331236.swipe_right,
12341237.swipe_left {
12351238 width: 25px;
@@ -1257,6 +1260,7 @@ body .panelControlBar {
12571260}
12581261
12591262.swipes-counter {
1263+ opacity: 0.3;
12601264 color: var(--SmartThemeBodyColor);
12611265 font-size: 12px;
12621266 padding: 0 5px;
@@ -1270,14 +1274,11 @@ body .panelControlBar {
12701274 height: var(--swipeCounterHeight);
12711275}
12721276
1273-body:not(.swipeAllMessages) .mes:not(.last_mes) .swipes-counter {
1274- visibility: hidden;
1275-}
12761277
12771278body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12781279 /* Avoid expensive DOM queries */
12791280 opacity: 0.3 !important;
12801281 display: flex !important;
12811282}
12821283
12831284.swipe_left {
@@ -1288,6 +1289,49 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
12881289.swipe_right {
12891290 right: 5px;
12901291 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 */
12911335}
12921336
12931337.ui-settings {
@@ -4445,11 +4489,12 @@ input[type="range"]::-webkit-slider-thumb {
44454489 field-sizing: content;
44464490}
44474491
44484492body: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) {
44534498 display: none;
44544499}
44554500