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, +780 -316Showing whitespace changes
public/css/animations.css+20 -0
@@ -132,3 +132,23 @@
132 overflow-y: hidden;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}
public/global.d.ts+6 -0
@@ -6,6 +6,7 @@ import { oai_settings } from './scripts/openai';
6import { textgenerationwebui_settings } from './scripts/textgen-settings';6import { textgenerationwebui_settings } from './scripts/textgen-settings';
7import { FileAttachment } from './scripts/chats';7import { FileAttachment } from './scripts/chats';
8import { ReasoningMessageExtra } from './scripts/reasoning';8import { ReasoningMessageExtra } from './scripts/reasoning';
9import { OVERSWIPE_BEHAVIOR } from './scripts/constants';
910
10declare global {11declare global {
11 // Custom types12 // Custom types
@@ -80,6 +81,9 @@ declare global {
80 title?: string;81 title?: string;
81 isSmallSys?: boolean;82 isSmallSys?: boolean;
82 token_count?: number;83 token_count?: number;
84 /** When false, the message cannot be swiped. */
85 swipeable?: boolean;
86 overswipe_behavior?: OVERSWIPE_BEHAVIOR;
83 files?: FileAttachment[];87 files?: FileAttachment[];
84 inline_image?: boolean;88 inline_image?: boolean;
85 media_display?: string;89 media_display?: string;
@@ -202,4 +206,6 @@ declare global {
202 rgba: string;206 rgba: string;
203 }207 }
204 };208 };
209
210 type SwipeEvent = JQuery.TriggeredEvent<any, any, HTMLElement, HTMLElement>;
205}211}
public/index.html+2 -2
@@ -7052,7 +7052,7 @@
7052 <div class="mes_timer"></div>7052 <div class="mes_timer"></div>
7053 <div class="tokenCounterDisplay"></div>7053 <div class="tokenCounterDisplay"></div>
7054 </div>7054 </div>
7055 <div class="swipe_left fa-solid fa-chevron-left" style="display: none;"></div>7055 <div class="swipe_left fa-solid fa-chevron-left"></div>
7056 <div class="mes_block">7056 <div class="mes_block">
7057 <div class="ch_name flex-container justifySpaceBetween">7057 <div class="ch_name flex-container justifySpaceBetween">
7058 <div class="flex-container flex1 alignitemscenter">7058 <div class="flex-container flex1 alignitemscenter">
@@ -7116,7 +7116,7 @@
7116 <div class="mes_bias"></div>7116 <div class="mes_bias"></div>
7117 </div>7117 </div>
7118 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">7118 <div class="flex-container swipeRightBlock flexFlowColumn flexNoGap">
7119 <div class="swipe_right fa-solid fa-chevron-right" style="display: none;"></div>7119 <div class="swipe_right fa-solid fa-chevron-right"></div>
7120 <div class="swipes-counter"></div>7120 <div class="swipes-counter"></div>
7121 </div>7121 </div>
7122 </div>7122 </div>
public/script.js+563 -230
@@ -181,8 +181,10 @@ import {
181 canUseNegativeLookbehind,181 canUseNegativeLookbehind,
182 trimSpaces,182 trimSpaces,
183 clamp,183 clamp,
184 shakeElement,
185 createTimeout,
184} from './scripts/utils.js';186} from './scripts/utils.js';
185import { debounce_timeout, GENERATION_TYPE_TRIGGERS, IGNORE_SYMBOL, inject_ids, MEDIA_DISPLAY, MEDIA_SOURCE, MEDIA_TYPE, SCROLL_BEHAVIOR, SWIPE_DIRECTION } from './scripts/constants.js';187import { 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
187import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';189import { cancelDebouncedMetadataSave, doDailyExtensionUpdatesCheck, extension_settings, initExtensions, loadExtensionSettings, runGenerationInterceptors } from './scripts/extensions.js';
188import { COMMENT_NAME_DEFAULT, CONNECT_API_MAP, executeSlashCommandsOnChatInput, initDefaultSlashCommands, isExecutingCommandsFromChatInput, pauseScriptExecution, stopScriptExecution, UNIQUE_APIS } from './scripts/slash-commands.js';190import { 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;
372export let name2 = systemUserName;374export let name2 = systemUserName;
373/** @type {ChatMessage[]} */375/** @type {ChatMessage[]} */
374export let chat = [];376export let chat = [];
375export 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 */
381export let swipeState = SWIPE_STATE.NONE;
376let chatSaveTimeout;382let chatSaveTimeout;
377let importFlashTimeout;383let importFlashTimeout;
378export let isChatSaving = false;384export let isChatSaving = false;
@@ -561,6 +567,7 @@ let chat_file_for_del = '';
561export let online_status = 'no_connection';567export let online_status = 'no_connection';
562568
563export let is_send_press = false; //Send generation569export let is_send_press = false; //Send generation
570export const isGenerating = () => (is_send_press || is_group_generating);
564571
565let this_del_mes = -1;572let this_del_mes = -1;
566573
@@ -574,7 +581,14 @@ export let settings;
574export let amount_gen = 80; //default max length of AI generated responses581export let amount_gen = 80; //default max length of AI generated responses
575export let max_context = 2048;582export let max_context = 2048;
576583
577var swipes = true;584/** User preference for swipeable messages */
585let swipes = true;
586/** Forcefully hide swipes. */
587export let swipesHidden = false;
588/** @type {{ now: number, direction: string }} */
589export let lastSwipeInfo = { now: performance.now(), direction: SWIPE_DIRECTION.RIGHT };
590export let recentSwipes = 0;
591
578export let extension_prompts = {};592export let extension_prompts = {};
579593
580export let main_api;// = "kobold";594export let main_api;// = "kobold";
@@ -1376,10 +1390,11 @@ export async function showMoreMessages(messagesToLoad = null) {
13761390
1377 while (messageId > 0 && count > 0) {1391 while (messageId > 0 && count > 0) {
1378 let newMessageId = messageId - 1;1392 let newMessageId = messageId - 1;
1379 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId });1393 addOneMessage(chat[newMessageId], { insertBefore: messageId >= chat.length ? null : messageId, scroll: false, forceId: newMessageId, showSwipes: false });
1380 count--;1394 count--;
1381 messageId--;1395 messageId--;
1382 }1396 }
1397 refreshSwipeButtons();
13831398
1384 if (messageId == 0) {1399 if (messageId == 0) {
1385 $('#show_more_messages').remove();1400 $('#show_more_messages').remove();
@@ -1410,7 +1425,7 @@ export async function printMessages() {
14101425
1411 chatElement.find('.mes').removeClass('last_mes');1426 chatElement.find('.mes').removeClass('last_mes');
1412 chatElement.find('.mes').last().addClass('last_mes');1427 chatElement.find('.mes').last().addClass('last_mes');
1413 refreshSwipeButtons();1428 refreshSwipeButtons(false, false);
1414 applyStylePins();1429 applyStylePins();
1415 scrollChatToBottom({ waitForFrame: true });1430 scrollChatToBottom({ waitForFrame: true });
1416 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());1431 delay(debounce_timeout.short).then(() => scrollOnMediaLoad());
@@ -1471,6 +1486,7 @@ export async function clearChat() {
1471 if (is_delete_mode) {1486 if (is_delete_mode) {
1472 $('#dialogue_del_mes_cancel').trigger('click');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 chatElement.children().remove();1490 chatElement.children().remove();
1475 if ($('.zoomed_avatar[forChar]').length) {1491 if ($('.zoomed_avatar[forChar]').length) {
1476 console.debug('saw avatars to remove');1492 console.debug('saw avatars to remove');
@@ -1575,9 +1591,17 @@ export async function reloadCurrentChat() {
1575 * Send the message currently typed into the chat box.1591 * Send the message currently typed into the chat box.
1576 */1592 */
1577export async function sendTextareaMessage() {1593export 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 if (is_send_press) return;1600 if (is_send_press) return;
1579 if (isExecutingCommandsFromChatInput) return;1601 if (isExecutingCommandsFromChatInput) return;
15801602
1603 hideSwipeButtons(); //Swipe buttons must be hidden now, otherwise concurrent generations are possible.
1604
1581 let generateType = 'normal';1605 let generateType = 'normal';
1582 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last1606 // "Continue on send" is activated when the user hits "send" (or presses enter) on an empty chat box, and the last
1583 // message was sent from a character (not the user or the system).1607 // message was sent from a character (not the user or the system).
@@ -1597,7 +1621,9 @@ export async function sendTextareaMessage() {
1597 await newAssistantChat({ temporary: false });1621 await newAssistantChat({ temporary: false });
1598 }1622 }
15991623
1600 return await Generate(generateType);1624 let generation = await Generate(generateType);
1625 showSwipeButtons();
1626 return generation;
1601}1627}
16021628
1603/**1629/**
@@ -2362,7 +2388,7 @@ export function addCopyToCodeBlocks(messageElement) {
2362 * @param {boolean} [options.scroll=true] Whether to scroll to the new message2388 * @param {boolean} [options.scroll=true] Whether to scroll to the new message
2363 * @param {number} [options.insertBefore=null] Message ID to insert the new message before2389 * @param {number} [options.insertBefore=null] Message ID to insert the new message before
2364 * @param {number} [options.forceId=null] Force the message ID2390 * @param {number} [options.forceId=null] Force the message ID
2365 * @param {boolean} [options.showSwipes=true] Whether to show swipe buttons2391 * @param {boolean} [options.showSwipes=true] Whether to refresh the swipe buttons.
2366 * @returns {void}2392 * @returns {void}
2367 */2393 */
2368export function addOneMessage(mes, { type = 'normal', insertAfter = null, scroll = true, insertBefore = null, forceId = null, showSwipes = true } = {}) {2394export 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 });
24872513
2488 if (type === 'swipe') {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 swipeMessage.attr('swipeid', params.swipeId);2516 swipeMessage.attr('swipeid', params.swipeId);
2492 swipeMessage.find('.mes_text').html(messageText).attr('title', title);2517 swipeMessage.find('.mes_text').html(messageText).attr('title', title);
2493 swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);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 swipeMessage.find('.tokenCounterDisplay').empty();2530 swipeMessage.find('.tokenCounterDisplay').empty();
2506 }2531 }
2507 } else {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 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);2534 appendMediaToMessage(mes, newMessage, scroll ? SCROLL_BEHAVIOR.ADJUST : SCROLL_BEHAVIOR.NONE);
2511 showSwipes && hideSwipeButtons();
2512 }2535 }
25132536
2514 addCopyToCodeBlocks(newMessage);2537 addCopyToCodeBlocks(newMessage);
25152538
2516 // Set the swipes counter for past messages, only visible if 'Show Swipes on All Message' is enabled2539 // Set the swipes counter for all non-user messages.
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 }
25222543
2523 if (showSwipes) {2544 //last_mes should always be updated.
2545 chatElement.find('.mes').removeClass('last_mes');
2524 chatElement.find('.mes').last().addClass('last_mes');2546 chatElement.find('.mes').last().addClass('last_mes');
2525 chatElement.find('.mes').eq(-2).removeClass('last_mes');2547 if (showSwipes) {
2526 refreshSwipeButtons();2548 refreshSwipeButtons();
2527 }2549 }
25282550
@@ -3275,7 +3297,7 @@ class StreamingProcessor {
3275 }3297 }
32763298
3277 markUIGenStopped() {3299 markUIGenStopped() {
3278 activateSendButtons();3300 unblockGeneration();
3279 }3301 }
32803302
3281 async onStartStreaming(text) {3303 async onStartStreaming(text) {
@@ -3404,14 +3426,14 @@ class StreamingProcessor {
3404 }3426 }
34053427
3406 async onFinishStreaming(messageId, text) {3428 async onFinishStreaming(messageId, text) {
3407 this.markUIGenStopped();
3408 await this.onProgressStreaming(messageId, text, true);3429 await this.onProgressStreaming(messageId, text, true);
3409 addCopyToCodeBlocks(chatElement.find(`.mes[mesid="${messageId}"]`));3430 const messageElement = chatElement.find(`.mes[mesid="${messageId}"]`);
3431 const message = chat[messageId];
3432 addCopyToCodeBlocks(messageElement);
34103433
3411 await this.reasoningHandler.finish(messageId);3434 await this.reasoningHandler.finish(messageId);
34123435
3413 if (Array.isArray(this.swipes) && this.swipes.length > 0) {3436 if (Array.isArray(this.swipes) && this.swipes.length > 0) {
3414 const message = chat[messageId];
3415 const swipeInfoExtra = structuredClone(message.extra ?? {});3437 const swipeInfoExtra = structuredClone(message.extra ?? {});
3416 delete swipeInfoExtra.token_count;3438 delete swipeInfoExtra.token_count;
3417 delete swipeInfoExtra.reasoning;3439 delete swipeInfoExtra.reasoning;
@@ -3424,15 +3446,20 @@ class StreamingProcessor {
3424 };3446 };
3425 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));3447 const swipeInfoArray = Array(this.swipes.length).fill().map(() => structuredClone(swipeInfo));
3426 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);3448 parseReasoningInSwipes(this.swipes, swipeInfoArray, message.extra?.reasoning_duration);
3427 chat[messageId].swipes.push(...this.swipes);3449 message.swipes.push(...this.swipes);
3428 chat[messageId].swipe_info.push(...swipeInfoArray);3450 message.swipe_info.push(...swipeInfoArray);
3429 }3451 }
34303452
3453 syncMesToSwipe(messageId);
3454 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3455
3431 if (Array.isArray(this.images) && this.images.length > 0) {3456 if (Array.isArray(this.images) && this.images.length > 0) {
3432 await processImageAttachment(chat[messageId], { imageUrls: this.images });3457 await processImageAttachment(message, { imageUrls: this.images });
3433 appendMediaToMessage(chat[messageId], $(this.messageDom));3458 appendMediaToMessage(message, $(this.messageDom));
3434 }3459 }
34353460
3461 this.markUIGenStopped();
3462
3436 if (this.type !== 'impersonate') {3463 if (this.type !== 'impersonate') {
3437 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);3464 await eventSource.emit(event_types.MESSAGE_RECEIVED, this.messageId, this.type);
3438 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);3465 await eventSource.emit(event_types.CHARACTER_MESSAGE_RENDERED, this.messageId, this.type);
@@ -3440,15 +3467,13 @@ class StreamingProcessor {
3440 await eventSource.emit(event_types.IMPERSONATE_READY, text);3467 await eventSource.emit(event_types.IMPERSONATE_READY, text);
3441 }3468 }
34423469
3443 syncMesToSwipe(messageId);3470 updateSwipeCounter(messageId, { message, messageElement });
3444 saveLogprobsForActiveMessage(this.messageLogprobs.filter(Boolean), this.continueMessage);
3445 await saveChatConditional();
3446 unblockGeneration();
34473471
3448 const isAborted = this.abortController.signal.aborted;3472 const isAborted = this.abortController.signal.aborted;
3449 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(text)) {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();
34523477
3453 playMessageSound();3478 playMessageSound();
3454 }3479 }
@@ -3458,7 +3483,6 @@ class StreamingProcessor {
3458 this.isStopped = true;3483 this.isStopped = true;
34593484
3460 this.markUIGenStopped();3485 this.markUIGenStopped();
3461 unblockGeneration();
34623486
3463 const noEmitTypes = ['swipe', 'impersonate', 'continue'];3487 const noEmitTypes = ['swipe', 'impersonate', 'continue'];
3464 if (!noEmitTypes.includes(this.type)) {3488 if (!noEmitTypes.includes(this.type)) {
@@ -5171,7 +5195,8 @@ export async function Generate(type, { automatic_trigger, force_name2, quiet_pro
5171 const isAborted = abortController && abortController.signal.aborted;5195 const isAborted = abortController && abortController.signal.aborted;
5172 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {5196 if (!isAborted && power_user.auto_swipe && generatedTextFiltered(getMessage)) {
5173 is_send_press = false;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 }
51765201
5177 console.debug('/api/chats/save called by /Generate');5202 console.debug('/api/chats/save called by /Generate');
@@ -5303,7 +5328,6 @@ function unblockGeneration(type) {
53035328
5304 is_send_press = false;5329 is_send_press = false;
5305 activateSendButtons();5330 activateSendButtons();
5306 showSwipeButtons();
5307 setGenerationProgress(0);5331 setGenerationProgress(0);
5308 flushEphemeralStoppingStrings();5332 flushEphemeralStoppingStrings();
5309 flushWIInjections();5333 flushWIInjections();
@@ -6358,6 +6382,47 @@ export async function saveReply({ type, getMessage, fromStreaming = false, title
6358}6382}
63596383
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 */
6389export 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 * 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).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 * If the swipe data is invalid in some way, this function will exit out without doing anything.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) {
6539export function activateSendButtons() {6604export function activateSendButtons() {
6540 is_send_press = false;6605 is_send_press = false;
6541 hideStopButton();6606 hideStopButton();
6607 showSwipeButtons();
6542 delete document.body.dataset.generating;6608 delete document.body.dataset.generating;
6543}6609}
65446610
@@ -6547,6 +6613,7 @@ export function activateSendButtons() {
6547 */6613 */
6548export function deactivateSendButtons() {6614export function deactivateSendButtons() {
6549 showStopButton();6615 showStopButton();
6616 hideSwipeButtons();
6550 document.body.dataset.generating = 'true';6617 document.body.dataset.generating = 'true';
6551}6618}
65526619
@@ -7583,8 +7650,11 @@ function updateMessage(div) {
7583 const mesElement = div.closest('.mes');7650 const mesElement = div.closest('.mes');
7584 const mes = chat[mesElement.attr('mesid')];7651 const mes = chat[mesElement.attr('mesid')];
75857652
7653 // editing old messages
7654 mes['extra'] ??= {};
7655
7586 let regexPlacement;7656 let regexPlacement;
7587 if (mes.is_user) {7657 if (mes?.is_user) {
7588 regexPlacement = regex_placement.USER_INPUT;7658 regexPlacement = regex_placement.USER_INPUT;
7589 } else if (mes.extra?.type === 'narrator') {7659 } else if (mes.extra?.type === 'narrator') {
7590 regexPlacement = regex_placement.SLASH_COMMAND;7660 regexPlacement = regex_placement.SLASH_COMMAND;
@@ -7614,15 +7684,11 @@ function updateMessage(div) {
7614 }7684 }
7615 mes['mes'] = text;7685 mes['mes'] = text;
7616 if (mes['swipe_id'] !== undefined) {7686 if (mes['swipe_id'] !== undefined) {
7687 ensureSwipes(mes);
7617 mes['swipes'][mes['swipe_id']] = text;7688 mes['swipes'][mes['swipe_id']] = text;
7618 }7689 }
76197690
7620 // editing old messages7691 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 mes.extra.bias = bias ?? null;7692 mes.extra.bias = bias ?? null;
7627 } else {7693 } else {
7628 mes.extra.bias = null;7694 mes.extra.bias = null;
@@ -7693,8 +7759,7 @@ export async function messageEdit(editMessageId) {
7693 this_edit_mes_id = editMessageId;7759 this_edit_mes_id = editMessageId;
7694 this_edit_mes_chname = editMessage.name || (editMessage.is_user ? name1 : name2);7760 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
7699 const chatScrollPosition = chatElement.scrollTop();7764 const chatScrollPosition = chatElement.scrollTop();
7700 const messageBlock = messageElement.find('.mes_block');7765 const messageBlock = messageElement.find('.mes_block');
@@ -7749,7 +7814,7 @@ async function messageEditCancel(messageId = this_edit_mes_id) {
7749 if (this?.classList?.contains('mes_edit_cancel')) {7814 if (this?.classList?.contains('mes_edit_cancel')) {
7750 thisMesDiv = $(this).closest('.mes');7815 thisMesDiv = $(this).closest('.mes');
7751 } else {7816 } else {
7752 thisMesDiv = chatElement.children().filter(`[mesid="${messageId}"]`);7817 thisMesDiv = chatElement.children('.mes').filter(`[mesid="${messageId}"]`);
7753 }7818 }
77547819
7755 const thisMesBlock = thisMesDiv.find('.mes_block');7820 const thisMesBlock = thisMesDiv.find('.mes_block');
@@ -7830,11 +7895,17 @@ async function messageEditMove(sourceId, targetId) {
7830 }7895 }
78317896
7832 updateViewMessageIds();7897 updateViewMessageIds();
7898 refreshSwipeButtons();
7833 await saveChatConditional();7899 await saveChatConditional();
7834 return true;7900 return true;
7835}7901}
78367902
7837async function messageEditDone(div) {7903async 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 let { mesBlock, text, mes, bias } = updateMessage(div);7909 let { mesBlock, text, mes, bias } = updateMessage(div);
7839 if (this_edit_mes_id == 0) {7910 if (this_edit_mes_id == 0) {
7840 text = substituteParams(text);7911 text = substituteParams(text);
@@ -8559,99 +8630,191 @@ export function callPopup(text, type, inputValue = '', { okButton, rows, wide, w
85598630
8560/**8631/**
8561 * Update the swipe counter for mesId.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 * @param {Number} mesId8634 * @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 */
8564export async function updateSwipeCounter(mesId) {8639export 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 const currentMessage = chatElement.children().filter(`[mesid="${mesId}"]`);8641 messageElement ??= 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}
85708652
8571/**8653/**
8572 * Swipe buttons are often toggled to update their position.8654 * Returns true if messages are generally swipeable.
8573 * This should be replaced with a more efficient function.8655 * @returns {boolean}
8574 */8656 */
8575export function refreshSwipeButtons() {8657export function isSwipingAllowed() {
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}
85798669
8580export 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 */
8678export 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);
8585 }8684 }
85868685
8587 if (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}
85968706
8597 // swipe_id should be set if alternate greetings are added8707/**
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 */
8716export 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 */
8743export 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 return;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]');
86018758
8602 //had to add this to make the swipe counter work8759 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 happen8770 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}
86398799/**
8640 //allows for writing individual swipe counters for past messages8800 * 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();8802export function showSwipeButtons() {
8803 swipesHidden = false;
8804 refreshSwipeButtons();
8643}8805}
86448806
8645/**8807/**
8808 * This function is misleadingly named. It blocks generation then refreshes the swipe buttons and counters.
8646 * @param {object} [options] Options8809 * @param {object} [options] Options
8647 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.8810 * @param {boolean} [options.hideCounters=false] Also hide the swipes counter.
8648 */8811 */
8649export function hideSwipeButtons({ hideCounters = false } = {}) {8812export function hideSwipeButtons({ hideCounters = false } = {}) {
8650 isSwipingAllowed = false;8813 swipesHidden = true;
8651 chatElement.find('.swipe_right').hide();8814 refreshSwipeButtons();
8652 chatElement.find('.swipe_left').hide();8815
8653 if (hideCounters === true) {8816 if (hideCounters === true) {
8654 chatElement.find('.last_mes .swipes-counter').hide();8817 chatElement.find('.last_mes .swipes-counter').prop('hidden', true);
8655 }8818 }
8656}8819}
86578820
@@ -8694,14 +8857,17 @@ export async function deleteSwipe(swipeId = null, messageId = chat.length - 1) {
86948857
8695 // Select the next swipe, or the one before if it was the last one8858 // Select the next swipe, or the one before if it was the last one
8696 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);8859 const newSwipeId = Math.min(swipeId, message.swipes.length - 1);
8697 syncSwipeToMes(messageId, newSwipeId);
86988860
8699 chat_metadata['tainted'] = true;8861 chat_metadata['tainted'] = true;
87008862
8863 messageId = Number(messageId);
8864 swipeId = Number(swipeId);
8701 await eventSource.emit(event_types.MESSAGE_SWIPE_DELETED, { messageId, swipeId, newSwipeId });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 });
87028869
8703 await saveChatConditional();8870 await saveChatConditional();
8704 await reloadCurrentChat();
87058871
8706 return newSwipeId;8872 return newSwipeId;
8707}8873}
@@ -8791,7 +8957,7 @@ export function updateEditArrowClasses() {
8791 return;8957 return;
8792 }8958 }
87938959
8794 const message = chatElement.find(`.mes[mesid="${this_edit_mes_id}"]`);8960 const message = chatElement.children('.mes').filter(`.mes[mesid="${this_edit_mes_id}"]`);
87958961
8796 const downButton = message.find('.mes_edit_down');8962 const downButton = message.find('.mes_edit_down');
8797 const upButton = message.find('.mes_edit_up');8963 const upButton = message.find('.mes_edit_up');
@@ -9226,6 +9392,23 @@ export async function createOrEditCharacter(e) {
9226}9392}
92279393
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 */
9399export 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 * Formats a counter for a swipe view.9412 * Formats a counter for a swipe view.
9230 * @param {number} current The current number of items.9413 * @param {number} current The current number of items.
9231 * @param {number} total The total number of items.9414 * @param {number} total The total number of items.
@@ -9240,41 +9423,61 @@ function formatSwipeCounter(current, total) {
92409423
9241/**9424/**
9242 * Handles the swipe event.9425 * Handles the swipe event.
9243 * @param {JQuery.Event} _event Event.9426 * @param {SwipeEvent} event Event.
9244 * @param {'left'|'right'} direction The direction to swipe.9427 * @param {'left'|'right'} direction The direction to swipe.
9245 * @param {object} params Additional parameters.9428 * @param {object} params Additional parameters.
9246 * @param {string} [params.source] The source of the swipe event.9429 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9247 * @param {boolean} [params.repeated] Is the swipe event repeated.9430 * @param {boolean} [params.repeated] Is the swipe event repeated.
9248 * @param {object} [params.message=chat[chat.length - 1]] The chat message to swipe.9431 * @param {ChatMessage} [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 */
9250export async function swipe(_event, direction, { source, repeated, message = chat[chat.length - 1] } = {}) {9436export async function swipe(event, direction, { source, repeated, message = chat[chat.length - 1], forceMesId, forceSwipeId, forceDuration } = {}) {
9251 if (chat.length === 0) {9437 if (chat.length === 0) {
9252 console.warn('Swipe was called on an empty chat.');9438 console.warn('Swipe was called on an empty chat.');
9253 return;9439 return;
9254 }9440 }
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;
9264 let messageIndex;9442 let messageIndex;
92659443
9266 //Only set messageIndex if message exists because -1 is truthy.9444 //Only set messageIndex if message exists because -1 is truthy.
9267 if (message) {9445 if (message) {
9268 messageIndex = chat.indexOf(message);9446 messageIndex = chat.indexOf(message);
9269 if (messageIndex === -1) {9447 if (messageIndex === -1 && typeof (forceMesId) != 'number') {
9270 console.error(`The message must exist in chat. ${message};`);9448 console.error(`The message must exist in chat. ${message};`);
9271 return;9449 return;
9272 }9450 }
9273 }9451 }
92749452
9275 const mesId = Number($(this).closest('.mes').attr('mesid') ?? messageIndex ?? chat.length - 1);9453 const mesId = Number(forceMesId ?? event?.currentTarget?.closest('.mes')?.getAttribute('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
9277 const thisMesDiv = chatElement.children().filter(`.mes[mesid="${mesId}"]`);9480 const thisMesDiv = chatElement.children('.mes').filter(`[mesid="${mesId}"]`);
9278 const thisMesText = thisMesDiv.find('.mes_block .mes_text');9481 const thisMesText = thisMesDiv.find('.mes_block .mes_text');
9279 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;9482 const thisMesDivHeight = thisMesDiv[0]?.scrollHeight;
9280 const thisMesTextHeight = thisMesText[0]?.scrollHeight;9483 const thisMesTextHeight = thisMesText[0]?.scrollHeight;
@@ -9283,43 +9486,108 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9283 return;9486 return;
9284 }9487 }
9285 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);9488 const originalSwipeId = Number(chat[mesId]?.['swipe_id'] ?? 0);
9286 let newSwipeId = Number(originalSwipeId);9489 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 }
92919510
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 //Wait for the generation to end.9522 //Wait for the generation to end.
9294 try {9523 try {
9524 //`mes_buttons` need to be hidden until the animation completes.
9525 if (generation) {
9526 document.body.dataset.swiping = 'true';
9295 await generation;9527 await generation;
9296 }9528 }
9529 }
9297 catch (error) {9530 catch (error) {
9298 console.warn(`Swipe failed, Swiping back. ${error}`);9531 console.warn(`Swipe failed, Swiping back. ${error}`);
9299 }9532 }
9300 //Allow for another swipe.
9301 showSwipeButtons();
93029533
9303 //Clamp Id between swipes.9534 //Clamp Id between swipes.
9304 let clampedId = clamp(chat[mesId]['swipe_id'], 0, Math.max(0, chat[mesId]['swipes'].length - 1));9535 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
9313 await updateSwipeCounter(mesId);9537 await updateSwipeCounter(mesId);
9314 //Fallback.9538 //Fallback.
9315 if (mesId != chat.length - 1) {9539 if (mesId != chat.length - 1) {
9316 await updateSwipeCounter(chat.length - 1);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 }
93199581
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 //If swipe_id has changed, or the source is being deleted.9589 //If swipe_id has changed, or the source is being deleted.
9322 if (newSwipeId !== originalSwipeId || source == 'delete') {9590 if (newSwipeId !== originalSwipeId || source == SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
9323 //Update the chat.9591 //Update the chat.
9324 await loadFromSwipeId(mesId, newSwipeId);9592 await loadFromSwipeId(mesId, newSwipeId);
9325 //Transition to the new chat.9593 //Transition to the new chat.
@@ -9329,6 +9597,27 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9329 }9597 }
93309598
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 * Sets the message to the newSwipeId and loads it.9621 * Sets the message to the newSwipeId and loads it.
9333 * @param {number} mesId9622 * @param {number} mesId
9334 * @param {number} newSwipeId9623 * @param {number} newSwipeId
@@ -9337,55 +9626,89 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9337 //Update the swipe_id.9626 //Update the swipe_id.
9338 chat[mesId]['swipe_id'] = newSwipeId;9627 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
9347 delete chat[mesId].extra.generationType;9636 chat[mesId].swipe_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 };
93679641
9368 /**9642 /**
9369 * Animates a swipe for all messages >= mesId.9643 * Animates a swipe for all messages >= mesId.
9370 * @param {number} mesId9644 * @param {number} mesId
9371 * @param {number} x9645 * @param {object} params
9372 * @param {number} duration9646 * @param {string} [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 }
93819705
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 }
93909713
9391 function getMessageBottomHeight(thisMesDiv) {9714 function getMessageBottomHeight(thisMesDiv) {
@@ -9427,11 +9750,15 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9427 /**9750 /**
9428 * Anime a swipe, optionally running a generation.9751 * Anime a swipe, optionally running a generation.
9429 * @param {boolean} run_generate9752 * @param {boolean} run_generate
9753 * @param {boolean} [skipSwipeOut=false]
9430 */9754 */
9431 async function animateSwipe(run_generate = false) {9755 async function animateSwipe(run_generate = false, skipSwipeOut = false) {
94329756
9757 if (!skipSwipeOut) {
9433 //Swipe out.9758 //Swipe out.
9434 await animateSwipeTransition(mesId, swipeRange, swipeDuration);9759 await animateSwipeTransition(mesId, { xEnd: `${swipeRange}px`, duration: swipeDuration });
9760 }
9761
94359762
9436 if (run_generate) {9763 if (run_generate) {
9437 await updateSwipeCounter(mesId);9764 await updateSwipeCounter(mesId);
@@ -9447,7 +9774,8 @@ export async function swipe(_event, direction, { source, repeated, message = cha
94479774
9448 //Only scroll when swiping the last message.9775 //Only scroll when swiping the last message.
9449 const scroll = (mesId == chat.length - 1);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 });
94519779
9452 if (power_user.message_token_count_enabled) {9780 if (power_user.message_token_count_enabled) {
9453 if (!chat[mesId].extra) {9781 if (!chat[mesId].extra) {
@@ -9465,10 +9793,6 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9465 thisMesDiv.css('height', thisMesDivHeight);9793 thisMesDiv.css('height', thisMesDivHeight);
9466 expandNewMessage(thisMesDiv);9794 expandNewMessage(thisMesDiv);
94679795
9468
9469 //Jump to the opposite side.
9470 await animateSwipeTransition(mesId, -swipeRange, 0);
9471
9472 appendMediaToMessage(chat[mesId], thisMesDiv);9796 appendMediaToMessage(chat[mesId], thisMesDiv);
94739797
9474 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));9798 await eventSource.emit(event_types.MESSAGE_SWIPED, (mesId));
@@ -9476,12 +9800,10 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9476 if (run_generate && !is_send_press) {9800 if (run_generate && !is_send_press) {
9477 is_send_press = true;9801 is_send_press = true;
9478 generation = Generate('swipe');9802 generation = Generate('swipe');
9479 } else if (Number(chat[mesId]['swipe_id']) !== chat[mesId]['swipes'].length) {
9480 saveChatDebounced();
9481 }9803 }
94829804
9483 //Swipe in.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 }
94869808
9487 if (mesId === Number(this_edit_mes_id)) {9809 if (mesId === Number(this_edit_mes_id)) {
@@ -9496,7 +9818,7 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9496 }9818 }
94979819
9498 //If the swipe is not being deleted.9820 //If the swipe is not being deleted.
9499 if (source != 'delete') {9821 if (source != SWIPE_SOURCE.DELETE && source != SWIPE_SOURCE.BACK) {
95009822
9501 // Make sure ad-hoc changes to extras are saved before swiping away9823 // Make sure ad-hoc changes to extras are saved before swiping away
9502 syncMesToSwipe(mesId);9824 syncMesToSwipe(mesId);
@@ -9515,19 +9837,19 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9515 }9837 }
9516 // If the user is holding down the key and we're at the last or first swipe, don't do anything.9838 // If the user is holding down the key and we're at the last or first swipe, don't do anything.
9517 let isLastSwipe = (direction === SWIPE_DIRECTION.RIGHT) ? (chat[mesId].swipe_id === Math.max(0, chat[mesId]['swipes'].length - 1)) : chat[mesId].swipe_id === 0;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 if (source === 'keyboard' && repeated && isLastSwipe) {9840 if (source === SWIPE_SOURCE.KEYBOARD && repeated && isLastSwipe) {
9519 await endSwipe();9841 await endSwipe();
9520 return;9842 return;
9521 }9843 }
9522 } else if (source == 'delete') {9844 } else if (source == SWIPE_SOURCE.DELETE || source == SWIPE_SOURCE.BACK) {
9523 //If the swipe is being deleted.9845 //If the swipe is being deleted or reverted.
9524 await standardSwipe();9846 await standardSwipe(newSwipeId);
9525 return;9847 return;
9526 }9848 }
95279849
9528 //If swiping left.9850 //If swiping left.
9529 if (direction === SWIPE_DIRECTION.LEFT) {9851 if (direction === SWIPE_DIRECTION.LEFT) {
9530 newSwipeId--;9852 if (forceSwipeId == null) newSwipeId--;
9531 //Loop to last swipe if negative.9853 //Loop to last swipe if negative.
9532 if (newSwipeId < 0) {9854 if (newSwipeId < 0) {
9533 newSwipeId = Math.max(0, chat[mesId]['swipes'].length - 1);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 await endSwipe();9861 await endSwipe();
9540 return;9862 return;
9541 }9863 }
9542 await standardSwipe();9864 await standardSwipe(newSwipeId);
9543 return;9865 return;
9544 }9866 }
9545 //If swiping right.9867 //If swiping right.
9546 else if (direction === SWIPE_DIRECTION.RIGHT) {9868 else if (direction === SWIPE_DIRECTION.RIGHT) {
9547 // make new slot in array9869 // make new slot in array
9548 newSwipeId++;9870 if (forceSwipeId == null) newSwipeId++;
95499871
9550 //Minimum of zero.9872 //Minimum of zero.
9551 if (newSwipeId < 0) {9873 if (newSwipeId < 0) {
@@ -9555,35 +9877,37 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9555 return;9877 return;
9556 }9878 }
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.
9559 if (newSwipeId >= chat[mesId]['swipes'].length && ((chat.length !== 1 || !isPristine))) {9881 if (newSwipeId >= chat[mesId]['swipes'].length) {
9560 newSwipeId = chat[mesId]['swipes'].length;9882 newSwipeId = chat[mesId]['swipes'].length;
95619883
9562 //Update the swipe_id.9884 //Update the swipe_id.
9563 chat[mesId]['swipe_id'] = newSwipeId;9885 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) {
9567 //Cancel swipe.9891 //Cancel swipe.
9568 chat[mesId]['swipe_id'] = originalSwipeId;9892 chat[mesId]['swipe_id'] = originalSwipeId;
9569 await endSwipe();9893 await endSwipe();
9570 return;9894 return;
9571 } else {9895 }
9572 //Generate.9896 //Regenerate the message
9573 await loadFromSwipeId(mesId, newSwipeId);9897 else if (overswipe == OVERSWIPE_BEHAVIOR.REGENERATE) {
9898 clearMessageData(chat[mesId]);
9574 let run_generate = true;9899 let run_generate = true;
9900 //Generate.
9575 await animateSwipe(run_generate);9901 await animateSwipe(run_generate);
9576 await endSwipe();9902 await endSwipe();
9577 return;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 newSwipeId = 0;9907 newSwipeId = 0;
9584 }9908 }
9585 }9909 }
9586 await standardSwipe();9910 await standardSwipe(newSwipeId);
9587 return;9911 return;
9588 }9912 }
9589}9913}
@@ -9591,28 +9915,28 @@ export async function swipe(_event, direction, { source, repeated, message = cha
9591/**9915/**
9592 * @deprecated Use `swipe` instead.9916 * @deprecated Use `swipe` instead.
9593 * Handles the swipe to the left event.9917 * Handles the swipe to the left event.
9594 * @param {JQuery.Event} _event Event.9918 * @param {SwipeEvent} [event] Event.
9595 * @param {object} params Additional parameters.9919 * @param {object} params Additional parameters.
9596 * @param {string} [params.source] The source of the swipe event.9920 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9597 * @param {boolean} [params.repeated] Is the swipe event repeated.9921 * @param {boolean} [params.repeated] Is the swipe event repeated.
9598 * @param {object} [params.message] The chat message to swipe.9922 * @param {object} [params.message] The chat message to swipe.
9599 */9923 */
9600export async function swipe_left(_event, { source, repeated, message } = {}) {9924export async function swipe_left(event, { source, repeated, message } = {}) {
9601 await swipe.call(this, _event, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });9925 await swipe.call(this, event, SWIPE_DIRECTION.LEFT, { source: source, repeated: repeated, message: message });
9602}9926}
96039927
9604/**9928/**
9605 * @deprecated Use `swipe` instead.9929 * @deprecated Use `swipe` instead.
9606 * Handles the swipe to the right event.9930 * Handles the swipe to the right event.
9607 * @param {JQuery.Event} [_event] Event.9931 * @param {SwipeEvent} [event] Event.
9608 * @param {object} params Additional parameters.9932 * @param {object} params Additional parameters.
9609 * @param {string} [params.source] The source of the swipe event.9933 * @param {import('./scripts/constants.js').SWIPE_SOURCE} [params.source] The source of the swipe event. null, 'keyboard', 'auto_swipe', 'back' or 'delete'.
9610 * @param {boolean} [params.repeated] Is the swipe event repeated.9934 * @param {boolean} [params.repeated] Is the swipe event repeated.
9611 * @param {object} [params.message] The chat message to swipe.9935 * @param {object} [params.message] The chat message to swipe.
9612 */9936 */
9613//MARK: swipe_right9937//MARK: swipe_right
9614export async function swipe_right(_event = null, { source, repeated, message } = {}) {9938export async function swipe_right(event = null, { source, repeated, message } = {}) {
9615 await swipe.call(this, _event, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });9939 await swipe.call(this, event, SWIPE_DIRECTION.RIGHT, { source: source, repeated: repeated, message: message });
9616}9940}
96179941
9618/**9942/**
@@ -10782,10 +11106,12 @@ jQuery(async function () {
10782 }11106 }
1078311107
10784 else if (id == 'option_regenerate') {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 if (is_send_press == false) {11114 if (is_send_press == false) {
10787 //hideSwipeButtons();
10788
10789 if (selected_group) {11115 if (selected_group) {
10790 regenerateGroup();11116 regenerateGroup();
10791 }11117 }
@@ -10804,7 +11130,14 @@ jQuery(async function () {
10804 }11130 }
1080511131
10806 else if (id == 'option_continue') {11132 else if (id == 'option_continue') {
10807 if (this_edit_mes_id >= 0) return; // don't proceed if editing a message11133 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
10809 if (is_send_press == false || fromSlashCommand) {11142 if (is_send_press == false || fromSlashCommand) {
10810 is_send_press = true;11143 is_send_press = true;
public/scripts/RossAscends-mods.js+5 -5
@@ -37,7 +37,7 @@ import { debounce, getStringHash, isValidUrl } from './utils.js';
37import { chat_completion_sources, oai_settings } from './openai.js';37import { chat_completion_sources, oai_settings } from './openai.js';
38import { getTokenCountAsync } from './tokenizers.js';38import { getTokenCountAsync } from './tokenizers.js';
39import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';39import { textgen_types, textgenerationwebui_settings as textgen_settings, getTextGenServer } from './textgen-settings.js';
40import { debounce_timeout } from './constants.js';40import { debounce_timeout, SWIPE_SOURCE } from './constants.js';
4141
42import { Popup } from './popup.js';42import { Popup } from './popup.js';
43import { accountStorage } from './util/AccountStorage.js';43import { accountStorage } from './util/AccountStorage.js';
@@ -1106,7 +1106,7 @@ export function initRossMods() {
11061106
1107 if (event.key == 'ArrowLeft') { //swipes left1107 if (event.key == 'ArrowLeft') { //swipes left
1108 if (1108 if (
1109 isSwipingAllowed &&1109 isSwipingAllowed() &&
1110 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active1110 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
1111 $('#send_textarea').val() === '' &&1111 $('#send_textarea').val() === '' &&
1112 $('#character_popup').css('display') === 'none' &&1112 $('#character_popup').css('display') === 'none' &&
@@ -1114,13 +1114,13 @@ export function initRossMods() {
1114 !isInputElementInFocus() &&1114 !isInputElementInFocus() &&
1115 !isModifiedKeyboardEvent(event)1115 !isModifiedKeyboardEvent(event)
1116 ) {1116 ) {
1117 $('.swipe_left:last').trigger('click', { source: 'keyboard', repeated: event.repeat });1117 $('.swipe_left:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
1118 return;1118 return;
1119 }1119 }
1120 }1120 }
1121 if (event.key == 'ArrowRight') { //swipes right1121 if (event.key == 'ArrowRight') { //swipes right
1122 if (1122 if (
1123 isSwipingAllowed &&1123 isSwipingAllowed() &&
1124 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active1124 !isNanogallery2LightboxActive() && // Check if lightbox is NOT active
1125 $('#send_textarea').val() === '' &&1125 $('#send_textarea').val() === '' &&
1126 $('#character_popup').css('display') === 'none' &&1126 $('#character_popup').css('display') === 'none' &&
@@ -1128,7 +1128,7 @@ export function initRossMods() {
1128 !isInputElementInFocus() &&1128 !isInputElementInFocus() &&
1129 !isModifiedKeyboardEvent(event)1129 !isModifiedKeyboardEvent(event)
1130 ) {1130 ) {
1131 $('.swipe_right:last').trigger('click', { source: 'keyboard', repeated: event.repeat });1131 $('.swipe_right:last').trigger('click', { source: SWIPE_SOURCE.KEYBOARD, repeated: event.repeat });
1132 return;1132 return;
1133 }1133 }
1134 }1134 }
public/scripts/constants.js+37 -0
@@ -140,9 +140,46 @@ export const SCROLL_BEHAVIOR = {
140};140};
141141
142/**142/**
143 * @enum {string}
144 * @readonly
145 */
146export 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 * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}}160 * @type {{readonly LEFT: 'left', readonly RIGHT: 'right'}}
144 */161 */
145export const SWIPE_DIRECTION = {162export const SWIPE_DIRECTION = {
146 LEFT: 'left',163 LEFT: 'left',
147 RIGHT: 'right',164 RIGHT: 'right',
148};165};
166
167/**
168 * @type {{readonly DELETE: 'delete', readonly KEYBOARD: 'keyboard', readonly BACK: 'back', readonly AUTO_SWIPE: 'auto_swipe'}}
169 */
170export 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 */
181export 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) {
2799 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {2799 while (getFirstDisplayedMessageId() > mesId && getFirstDisplayedMessageId() !== 0) {
2800 await showMoreMessages();2800 await showMoreMessages();
2801 await delay(1);2801 await delay(1);
2802 target = $('#chat').find(`.mes[mesid=${mesId}]`);2802 target = $('#chat').find(`.mes[mesid="${mesId}"]`);
28032803
2804 if (target.length) {2804 if (target.length) {
2805 break;2805 break;
public/scripts/st-context.js+5 -1
@@ -57,7 +57,9 @@ import {
57 hideSwipeButtons,57 hideSwipeButtons,
58 deleteMessage,58 deleteMessage,
59 refreshSwipeButtons,59 refreshSwipeButtons,
60 swipe,
60 isSwipingAllowed,61 isSwipingAllowed,
62 swipeState,
61 ensureMessageMediaIsArray,63 ensureMessageMediaIsArray,
62 getMediaDisplay,64 getMediaDisplay,
63 getMediaIndex,65 getMediaIndex,
@@ -218,10 +220,12 @@ export function getContext() {
218 swipe: {220 swipe: {
219 left: swipe_left,221 left: swipe_left,
220 right: swipe_right,222 right: swipe_right,
223 to: swipe,
221 show: showSwipeButtons,224 show: showSwipeButtons,
222 hide: hideSwipeButtons,225 hide: hideSwipeButtons,
223 refresh: refreshSwipeButtons,226 refresh: refreshSwipeButtons,
224 isAllowed: () => isSwipingAllowed,227 isAllowed: isSwipingAllowed,
228 state: () => swipeState,
225 },229 },
226 variables: {230 variables: {
227 local: {231 local: {
public/scripts/system-messages.js+39 -62
@@ -1,3 +1,4 @@
1import { lodash } from '../lib.js';
1import { addOneMessage, chat, displayVersion, setSendButtonState, system_avatar, systemUserName } from '../script.js';2import { addOneMessage, chat, displayVersion, setSendButtonState, system_avatar, systemUserName } from '../script.js';
2import { t } from './i18n.js';3import { t } from './i18n.js';
3import { getMessageTimeStamp } from './RossAscends-mods.js';4import { getMessageTimeStamp } from './RossAscends-mods.js';
@@ -30,89 +31,67 @@ export const system_message_types = {
30};31};
3132
32export async function initSystemMessages() {33export async function initSystemMessages() {
33 /** @type {Record<string, ChatMessage>} */34 /** @type {ChatMessage} */
34 const result = {35 const defaultMessage = {
35 help: {
36 name: systemUserName,36 name: systemUserName,
37 force_avatar: system_avatar,37 force_avatar: system_avatar,
38 is_user: false,38 is_user: false,
39 is_system: true,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 mes: await renderTemplateAsync('help'),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 mes: '',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 mes: await renderTemplateAsync('hotkeys'),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 mes: await renderTemplateAsync('formatting'),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 mes: await renderTemplateAsync('macros'),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 mes: await renderTemplateAsync('welcome', { displayVersion }),66 mes: await renderTemplateAsync('welcome', { displayVersion }),
76 extra: {67 extra: {
77 uses_system_ui: true,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 mes: 'No one hears you. <b>Hint&#58;</b> add more members to the group!',73 mes: 'No one hears you. <b>Hint&#58;</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 mes: 'Generic system message. User `text` parameter to override the contents',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 mes: await renderTemplateAsync('welcomePrompt'),81 mes: await renderTemplateAsync('welcomePrompt'),
100 extra: {82 extra: {
101 uses_system_ui: true,83 uses_system_ui: true,
102 isSmallSys: true,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 mes: await renderTemplateAsync('assistantNote'),89 mes: await renderTemplateAsync('assistantNote'),
111 extra: {90 extra: {
112 uses_system_ui: true,91 uses_system_ui: true,
113 isSmallSys: true,92 isSmallSys: true,
114 },93 },
115 },94 }),
116 };95 };
11796
118 Object.assign(system_messages, result);97 Object.assign(system_messages, result);
@@ -132,6 +111,8 @@ export async function initSystemMessages() {
132111
133/**112/**
134 * Gets a system message by type.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 * @param {string} type Type of system message116 * @param {string} type Type of system message
136 * @param {string} [text] Text to be sent117 * @param {string} [text] Text to be sent
137 * @param {ChatMessageExtra} [extra] Additional data to be added to the message118 * @param {ChatMessageExtra} [extra] Additional data to be added to the message
@@ -154,10 +135,6 @@ export function getSystemMessageByType(type, text, extra = {}) {
154 newMessage.mes = getSlashCommandsHelp();135 newMessage.mes = getSlashCommandsHelp();
155 }136 }
156137
157 if (!newMessage.extra || typeof newMessage.extra !== 'object') {
158 newMessage.extra = {};
159 }
160
161 newMessage.extra = Object.assign(newMessage.extra, extra);138 newMessage.extra = Object.assign(newMessage.extra, extra);
162 newMessage.extra.type = type;139 newMessage.extra.type = type;
163 return newMessage;140 return newMessage;
public/scripts/utils.js+41 -0
@@ -2842,4 +2842,45 @@ export async function importFromExternalUrl(url, { preserveFileName = null } = {
2842 }2842 }
2843}2843}
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 */
2845export const clamp = (value, min, max) => Math.min(Math.max(value, min), max);2853export 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 */
2862export 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 */
2881export 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() {
135 send_date: getMessageTimeStamp(),135 send_date: getMessageTimeStamp(),
136 extra: {136 extra: {
137 type: system_message_types.ASSISTANT_MESSAGE,137 type: system_message_types.ASSISTANT_MESSAGE,
138 swipeable: false,
138 },139 },
139 };140 };
140141
public/style.css+60 -15
@@ -677,11 +677,6 @@ small {
677 margin: 0;677 margin: 0;
678}678}
679679
680.mes.smallSysMes .swipe_right,
681.mes.smallSysMes .swipe_left {
682 display: none !important;
683}
684
685.mes.smallSysMes .mes_text {680.mes.smallSysMes .mes_text {
686 padding: 0 !important;681 padding: 0 !important;
687 text-align: center;682 text-align: center;
@@ -1230,6 +1225,14 @@ body .panelControlBar {
1230 --swipeCounterMargin: 5px;1225 --swipeCounterMargin: 5px;
1231}1226}
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
1233.swipe_right,1236.swipe_right,
1234.swipe_left {1237.swipe_left {
1235 width: 25px;1238 width: 25px;
@@ -1257,6 +1260,7 @@ body .panelControlBar {
1257}1260}
12581261
1259.swipes-counter {1262.swipes-counter {
1263 opacity: 0.3;
1260 color: var(--SmartThemeBodyColor);1264 color: var(--SmartThemeBodyColor);
1261 font-size: 12px;1265 font-size: 12px;
1262 padding: 0 5px;1266 padding: 0 5px;
@@ -1270,14 +1274,11 @@ body .panelControlBar {
1270 height: var(--swipeCounterHeight);1274 height: var(--swipeCounterHeight);
1271}1275}
12721276
1273body:not(.swipeAllMessages) .mes:not(.last_mes) .swipes-counter {
1274 visibility: hidden;
1275}
12761277
1277body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {1278body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1278 /* Avoid expensive DOM queries */1279 /* Avoid expensive DOM queries */
1279 opacity: 0.3 !important;1280 opacity: 0.3;
1280 display: flex !important;1281 display: flex;
1281}1282}
12821283
1283.swipe_left {1284.swipe_left {
@@ -1288,6 +1289,49 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter {
1288.swipe_right {1289.swipe_right {
1289 right: 5px;1290 right: 5px;
1290 align-self: center;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. */
1320body: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. */
1326body.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}
12921336
1293.ui-settings {1337.ui-settings {
@@ -4445,11 +4489,12 @@ input[type="range"]::-webkit-slider-thumb {
4445 field-sizing: content;4489 field-sizing: content;
4446}4490}
44474491
4448body[data-generating="true"] #send_but,4492body:is([data-generating="true"], [data-swiping="true"]) :is(
4449body[data-generating="true"] #mes_continue,4493 #send_but,
4450body[data-generating="true"] #mes_impersonate,4494 #mes_continue,
4451body[data-generating="true"] #chat .last_mes .mes_buttons,4495 #mes_impersonate,
4452body[data-generating="true"] #chat .last_mes .mes_reasoning_actions {4496 #chat .last_mes .mes_buttons,
4497 #chat .last_mes .mes_reasoning_actions) {
4453 display: none;4498 display: none;
4454}4499}
44554500