Merge pull request #3346 from SillyTavern/persona-improvements Expand Persona links/locking to characters and allowing more control on which persona to choose/load
Signed| @@ -96,11 +96,6 @@ body.charListGrid #rm_print_characters_block .group_select .group_name_block, | ||
| 96 | 96 | flex-direction: column; |
| 97 | 97 | } |
| 98 | 98 | |
| 99 | -#user_avatar_block.gridView .avatar-container .avatar-buttons { | |
| 100 | - flex-wrap: wrap; | |
| 101 | - justify-content: space-evenly; | |
| 102 | -} | |
| 103 | - | |
| 104 | 99 | body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container, |
| 105 | 100 | body.charListGrid #rm_print_characters_block .character_select .character_select_container, |
| 106 | 101 | body.charListGrid #rm_print_characters_block .group_select .group_select_container, |
| @@ -231,10 +226,16 @@ body.big-avatars .avatars_inline_small .avatar img { | ||
| 231 | 226 | body.big-avatars .avatars_inline { |
| 232 | 227 | max-height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) + 2 * var(--avatar-base-border-radius)); |
| 233 | 228 | } |
| 229 | +body.big-avatars .avatars_inline.avatars_multiline { | |
| 230 | + max-height: fit-content; | |
| 231 | +} | |
| 234 | 232 | |
| 235 | 233 | body.big-avatars .avatars_inline.avatars_inline_small { |
| 236 | 234 | height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-small-factor) + 2 * var(--avatar-base-border-radius)); |
| 237 | 235 | } |
| 236 | +body.big-avatars .avatars_inline.avatars_inline_small.avatars_multiline { | |
| 237 | + height: inherit; | |
| 238 | +} | |
| 238 | 239 | |
| 239 | 240 | body:not(.big-avatars) .avatars_inline_small .avatar_collage { |
| 240 | 241 | min-width: calc(var(--avatar-base-width) * var(--inline-avatar-small-factor)); |
| @@ -4987,7 +4987,7 @@ | ||
| 4987 | 4987 | </div> |
| 4988 | 4988 | </div> |
| 4989 | 4989 | <div id="persona-management-block" class="flex-container wide100p flexGap10"> |
| 4990 | 4990 | <div class="persona_management_left_column flex1 overflowHidden wide100p"> |
| 4991 | 4991 | <div class="flex-container marginBot10 alignitemscenter"> |
| 4992 | 4992 | <div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona" data-i18n="[title]Create a dummy persona"> |
| 4993 | 4993 | <i class="fa-solid fa-person-circle-question fa-fw"></i> |
| @@ -5010,27 +5010,34 @@ | ||
| 5010 | 5010 | <input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value=""> |
| 5011 | 5011 | </form> |
| 5012 | 5012 | </div> |
| 5013 | 5013 | <div class="persona_management_right_column flex1"> |
| 5014 | 5014 | <h4div data-i18nclass="Namepersona_management_current_persona">Name</h4> |
| 5015 | - <div class="change_name"> | |
| 5015 | + <h4 class="standoutHeader" data-i18n="Current Persona">Current Persona</h4> | |
| 5016 | - <input id="your_name" name="your_name" data-i18n="[placeholder]Enter your name" placeholder="Enter your name" class="text_pole wide100p" value="" autocomplete="off"> | |
| 5016 | + | |
| 5017 | - <div id="your_name_button" class="menu_button fa-solid fa-check" title="Click to set a new User Name" data-i18n="[title]Click to set a new User Name"> | |
| 5017 | + <div id="persona_controls" class="flex-container"> | |
| 5018 | - </div> | |
| 5018 | + <h5 id="your_name" class="persona_name">[Persona Name]</h5> | |
| 5019 | - <div id="lock_user_name" class="menu_button fa-solid fa-unlock" title="Click to lock your selected persona to the current chat. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current chat. Click again to remove the lock."> | |
| 5019 | + <div class="persona_controls_buttons_block buttons_block"> | |
| 5020 | - </div> | |
| 5020 | + <div id="persona_rename_button" class="menu_button fa-solid fa-pencil" title="Rename Persona" data-i18n="[title]Rename Persona"></div> | |
| 5021 | 5021 | <div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages" data-i18n="[title]Click to set user name for all messages"></div> |
| 5022 | - </div> | |
| 5022 | + <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook"></div> | |
| 5023 | - <div id="persona_lore_button" class="menu_button fa-solid fa-globe" title="Persona Lore Alt+Click to open the lorebook" data-i18n="[title]Persona Lore Alt+Click to open the lorebook"> | |
| 5023 | + | |
| 5024 | + <div id="persona_set_image_button" class="menu_button fa-solid fa-image" title="Change Persona Image" data-i18n="[title]Change Persona Image"></div> | |
| 5025 | + <div id="persona_duplicate_button" class="menu_button fa-solid fa-clone" title="Duplicate Persona" data-i18n="[title]Duplicate Persona"></div> | |
| 5026 | + <div id="persona_delete_button" class="menu_button fa-solid fa-skull red_button" title="Delete Persona" data-i18n="[title]Delete Persona"></div> | |
| 5027 | + </div> | |
| 5024 | 5028 | </div> |
| 5025 | - </div> | |
| 5029 | + | |
| 5026 | - <div> | |
| 5027 | 5030 | <h4 data-i18n="Persona Description">Persona Description</h4> |
| 5028 | 5031 | <textarea id="persona_description" name="persona_description" data-i18n="[placeholder]Example: [{{user}} is a 28-year-old Romanian cat girl.]" placeholder="Example: [{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" value="" autocomplete="off" rows="8"></textarea> |
| 5029 | - <div class="extension_token_counter"> | |
| 5032 | + | |
| 5030 | - <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span> | |
| 5033 | + <div class="flex-container justifySpaceBetween"> | |
| 5034 | + <h4 data-i18n="Position">Position</h4> | |
| 5035 | + <div class="extension_token_counter widthFitContent"> | |
| 5036 | + <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span> | |
| 5037 | + </div> | |
| 5031 | 5038 | </div> |
| 5032 | - <div> | |
| 5039 | + | |
| 5033 | - <label for="persona_description_position" data-i18n="Position:">Position:</label> | |
| 5040 | + <div class="persona_management_description_position_container"> | |
| 5034 | 5041 | <select id="persona_description_position"> |
| 5035 | 5042 | <option value="9" data-i18n="None (disabled)">None (disabled)</option> |
| 5036 | 5043 | <option value="0" data-i18n="In Story String / Prompt Manager">In Story String / Prompt Manager</option> |
| @@ -5053,14 +5060,54 @@ | ||
| 5053 | 5060 | </div> |
| 5054 | 5061 | </div> |
| 5055 | 5062 | </div> |
| 5063 | + | |
| 5064 | + <h4 data-i18n="Connections">Connections</h4> | |
| 5065 | + <div id="persona_connections_buttons" class="flex-container"> | |
| 5066 | + <div id="lock_persona_default" class="menu_button menu_button_icon" title="Click to select this as default persona for the new chats. Click again to remove it." data-i18n="[title]Click to select this as default persona for the new chats. Click again to remove it."> | |
| 5067 | + <i class="icon fa-solid fa-crown fa-fw"></i> | |
| 5068 | + <div data-i18n="Default">Default</div> | |
| 5069 | + </div> | |
| 5070 | + <div id="lock_user_name" class="menu_button menu_button_icon" title="Click to lock your selected persona to the current chat. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current chat. Click again to remove the lock."> | |
| 5071 | + <i class="icon fa-solid fa-unlock fa-fw"></i> | |
| 5072 | + <div data-i18n="Chat">Chat</div> | |
| 5073 | + </div> | |
| 5074 | + <div id="lock_persona_to_char" class="menu_button menu_button_icon" title="Click to lock your selected persona to the current character. Click again to remove the lock." data-i18n="[title]Click to lock your selected persona to the current character. Click again to remove the lock."> | |
| 5075 | + <i class="icon fa-solid fa-unlock fa-fw"></i> | |
| 5076 | + <div data-i18n="Character">Character</div> | |
| 5077 | + </div> | |
| 5078 | + </div> | |
| 5079 | + <div id="persona_connections_info_block"></div> | |
| 5080 | + <div id="persona_connections_list" class="text_muted m-b-1 avatars_inline avatars_multiline scroll-reset-container expander"> | |
| 5081 | + </div> | |
| 5056 | 5082 | </div> |
| 5057 | - <div class="range-block"> | |
| 5083 | + | |
| 5058 | 5084 | <label for="persona_show_notifications"div class="checkbox_labelpersona_management_global_settings"> |
| 5059 | 5085 | <inputh4 idclass="persona_show_notificationsstandoutHeader" typedata-i18n="checkboxGlobal Settings">Global Settings</h4> |
| 5060 | - <span data-i18n="Show notifications on switching personas"> | |
| 5086 | + | |
| 5061 | - Show notifications on switching personas | |
| 5087 | + <div class="range-block"> | |
| 5062 | - </span> | |
| 5088 | + <label for="persona_show_notifications" class="checkbox_label"> | |
| 5063 | - </label> | |
| 5089 | + <input id="persona_show_notifications" type="checkbox" /> | |
| 5090 | + <span data-i18n="Show notifications on switching personas"> | |
| 5091 | + Show notifications on switching personas | |
| 5092 | + </span> | |
| 5093 | + </label> | |
| 5094 | + </div> | |
| 5095 | + <div class="range-block"> | |
| 5096 | + <label for="persona_allow_multi_connections" class="checkbox_label" title="When multiple personas are connected to a character, a popup will appear to select which one to use." data-i18n="[title]When multiple personas are connected to a character, a popup will appear to select which one to use"> | |
| 5097 | + <input id="persona_allow_multi_connections" type="checkbox" /> | |
| 5098 | + <span data-i18n="Allow multiple persona connections per character"> | |
| 5099 | + Allow multiple persona connections per character | |
| 5100 | + </span> | |
| 5101 | + </label> | |
| 5102 | + </div> | |
| 5103 | + <div class="range-block"> | |
| 5104 | + <label for="persona_auto_lock" class="checkbox_label" title="Whenever a persona is selected, it will be locked to the current chat and automatically selected when the chat is opened." data-i18n="[title]Whenever a persona is selected, it will be locked to the current chat and automatically selected when the chat is opened."> | |
| 5105 | + <input id="persona_auto_lock" type="checkbox" /> | |
| 5106 | + <span data-i18n="Auto-lock a chosen persona to the chat"> | |
| 5107 | + Auto-lock a chosen persona to the chat | |
| 5108 | + </span> | |
| 5109 | + </label> | |
| 5110 | + </div> | |
| 5064 | 5111 | </div> |
| 5065 | 5112 | </div> |
| 5066 | 5113 | </div> |
| @@ -5087,7 +5134,7 @@ | ||
| 5087 | 5134 | <div class="right_menu_button fa-solid fa-list-ul" id="rm_button_characters" title="Select/Create Characters" data-i18n="[title]Select/Create Characters"></div> |
| 5088 | 5135 | </div> |
| 5089 | 5136 | <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p"> |
| 5090 | 5137 | <div class="hotswap avatars_inline flex-container scroll-reset-container expander" data-i18n="[no_favs]Favorite characters to add them to HotSwaps" no_favs="Favorite characters to add them to HotSwaps"></div> |
| 5091 | 5138 | </div> |
| 5092 | 5139 | </div> |
| 5093 | 5140 | <hr> |
| @@ -5125,13 +5172,13 @@ | ||
| 5125 | 5172 | </div> |
| 5126 | 5173 | </div> |
| 5127 | 5174 | <div class="flex-container flexFlowColumn expander flexNoGap"> |
| 5128 | 5175 | <div id="avatar_div" class="avatar_div buttons_block alignitemsflexstart justifySpaceBetween flexnowrap"> |
| 5129 | 5176 | <label id="avatar_div_div" class="add_avatar avatar" for="add_avatar_button" title="Click to select a new avatar for this character" data-i18n="[title]Click to select a new avatar for this character"> |
| 5130 | 5177 | <img id="avatar_load_preview" src="img/ai4.png" alt="avatar"> |
| 5131 | 5178 | <input hidden type="file" id="add_avatar_button" name="avatar" accept="image/*"> |
| 5132 | 5179 | </label> |
| 5133 | 5180 | <div class="flex-container" id="avatar_controls"> |
| 5134 | 5181 | <div class="form_create_bottom_buttons_block buttons_block"> |
| 5135 | 5182 | <div id="rm_button_back" class="menu_button fa-solid fa-left-long "></div> |
| 5136 | 5183 | <!-- <div id="renameCharButton" class="menu_button fa-solid fa-user-pen" title="Rename Character"></div> --> |
| 5137 | 5184 | <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div> |
| @@ -5139,6 +5186,7 @@ | ||
| 5139 | 5186 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> |
| 5140 | 5187 | <div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore Click to load Shift-click to open 'Link to World Info' popup" data-i18n="[title]world_button_title"></div> |
| 5141 | 5188 | <div class="chat_lorebook_button menu_button fa-solid fa-passport" title="Chat Lore Alt+Click to open the lorebook" data-i18n="[title]Chat Lore Alt+Click to open the lorebook"></div> |
| 5189 | + <div id="char_connections_button" class="menu_button fa-solid fa-face-smile" title="Connected Personas" data-i18n="[title]Connected Personas"></div> | |
| 5142 | 5190 | <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> |
| 5143 | 5191 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> |
| 5144 | 5192 | <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> |
| @@ -5146,7 +5194,7 @@ | ||
| 5146 | 5194 | <label for="create_button" id="create_button_label" class="menu_button fa-solid fa-user-check" title="Create Character" data-i18n="[title]Create Character"> |
| 5147 | 5195 | <input type="submit" id="create_button" name="create_button"> |
| 5148 | 5196 | </label> |
| 5149 | 5197 | <div id="delete_button" class="menu_button fa-solid fa-skull red_button" title="Delete Character" data-i18n="[title]Delete Character"></div> |
| 5150 | 5198 | </div> |
| 5151 | 5199 | <label class="flex1 height100p" for="char-management-dropdown"> |
| 5152 | 5200 | <select id="char-management-dropdown" class="text_pole"> |
| @@ -5483,25 +5531,20 @@ | ||
| 5483 | 5531 | <div class="flex-container wide100pLess70px character_select_container"> |
| 5484 | 5532 | <div class="wide100p character_name_block"> |
| 5485 | 5533 | <span class="ch_name flex1"></span> |
| 5486 | - <div class="avatar-buttons"> | |
| 5487 | - <button class="menu_button bind_user_name" title="Bind user name to that avatar" data-i18n="[title]Bind user name to that avatar"> | |
| 5488 | - <i class="fa-fw fa-solid fa-user-edit fa-sm"></i> | |
| 5489 | - </button> | |
| 5490 | - <button class="menu_button set_persona_image" title="Change persona image" data-i18n="[title]Change persona image"> | |
| 5491 | - <i class="fa-fw fa-solid fa-image fa-sm"></i> | |
| 5492 | - </button> | |
| 5493 | - <button class="menu_button set_default_persona" title="Select this as default persona for the new chats." data-i18n="[title]Select this as default persona for the new chats."> | |
| 5494 | - <i class="fa-fw fa-solid fa-crown fa-sm"></i> | |
| 5495 | - </button> | |
| 5496 | - <button class="menu_button duplicate_persona" title="Duplicate persona" data-i18n="[title]Duplicate persona"> | |
| 5497 | - <i class="fa-fw fa-solid fa-clone fa-sm"></i> | |
| 5498 | - </button> | |
| 5499 | - <button class="menu_button delete_avatar" title="Delete persona" data-i18n="[title]Delete persona"> | |
| 5500 | - <i class="fa-fw fa-solid fa-trash-alt fa-sm"></i> | |
| 5501 | - </button> | |
| 5502 | - </div> | |
| 5503 | 5534 | </div> |
| 5504 | 5535 | <div class="ch_description"></div> |
| 5536 | + <div class="avatar_container_states buttons_block"> | |
| 5537 | + <div class="locked_to_chat_label avatar_state has_hover_label menu_button menu_button_icon disabled" title="Persona is locked to the current chat" data-i18n="[title]Persona is locked to the current chat"> | |
| 5538 | + <i class="icon fa-solid fa-lock fa-fw"></i> | |
| 5539 | + <i class="label_icon icon fa-solid fa-comments fa-fw"></i> | |
| 5540 | + <div class="label" data-i18n="Chat">Chat</div> | |
| 5541 | + </div> | |
| 5542 | + <div class="locked_to_character_label avatar_state has_hover_label menu_button menu_button_icon disabled " title="Persona is locked to the current character" data-i18n="[title]Persona is locked to the current character"> | |
| 5543 | + <i class="icon fa-solid fa-lock fa-fw"></i> | |
| 5544 | + <i class="label_icon icon fa-solid fa-user fa-fw"></i> | |
| 5545 | + <div class="label" data-i18n="Character">Character</div> | |
| 5546 | + </div> | |
| 5547 | + </div> | |
| 5505 | 5548 | </div> |
| 5506 | 5549 | </div> |
| 5507 | 5550 | </div> |
| @@ -235,6 +235,8 @@ import { | ||
| 235 | 235 | initPersonas, |
| 236 | 236 | setPersonaDescription, |
| 237 | 237 | initUserAvatar, |
| 238 | + updatePersonaConnectionsAvatarList, | |
| 239 | + isPersonaPanelOpen, | |
| 238 | 240 | } from './scripts/personas.js'; |
| 239 | 241 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 240 | 242 | import { hideLoader, showLoader } from './scripts/loader.js'; |
| @@ -983,7 +985,7 @@ async function firstLoadInit() { | ||
| 983 | 985 | await initTokenizers(); |
| 984 | 986 | initBackgrounds(); |
| 985 | 987 | initAuthorsNote(); |
| 986 | 988 | await initPersonas(); |
| 987 | 989 | initRossMods(); |
| 988 | 990 | initStats(); |
| 989 | 991 | initCfg(); |
| @@ -1362,6 +1364,14 @@ export function resultCheckStatus() { | ||
| 1362 | 1364 | stopStatusLoading(); |
| 1363 | 1365 | } |
| 1364 | 1366 | |
| 1367 | + | |
| 1368 | +/** | |
| 1369 | + * Switches the currently selected character to the one with the given ID. (character index, not the character key!) | |
| 1370 | + * | |
| 1371 | + * If the character ID doesn't exist, if the chat is being saved, or if a group is being generated, this function does nothing. | |
| 1372 | + * If the character is different from the currently selected one, it will clear the chat and reset any selected character or group. | |
| 1373 | + * @param {number} id The ID of the character to switch to. | |
| 1374 | + */ | |
| 1365 | 1375 | export async function selectCharacterById(id) { |
| 1366 | 1376 | if (characters[id] === undefined) { |
| 1367 | 1377 | return; |
| @@ -1435,7 +1445,7 @@ function getCharacterBlock(item, id) { | ||
| 1435 | 1445 | } |
| 1436 | 1446 | // Populate the template |
| 1437 | 1447 | const template = $('#character_template .character_select').clone(); |
| 1438 | 1448 | template.attr({ 'data-chid': id, 'id': `CharID${id}` }); |
| 1439 | 1449 | template.find('img').attr('src', this_avatar).attr('alt', item.name); |
| 1440 | 1450 | template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`); |
| 1441 | 1451 | template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`); |
| @@ -1561,6 +1571,7 @@ export async function printCharacters(fullRefresh = false) { | ||
| 1561 | 1571 | }); |
| 1562 | 1572 | |
| 1563 | 1573 | favsToHotswap(); |
| 1574 | + updatePersonaConnectionsAvatarList(); | |
| 1564 | 1575 | } |
| 1565 | 1576 | |
| 1566 | 1577 | /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ |
| @@ -6331,7 +6342,7 @@ export async function renameCharacter(name = null, { silent = false, renameChats | ||
| 6331 | 6342 | if (newChId !== -1) { |
| 6332 | 6343 | // Select the character after the renaming |
| 6333 | 6344 | this_chid = -1; |
| 6334 | 6345 | await selectCharacterById(String(newChId)); |
| 6335 | 6346 | |
| 6336 | 6347 | // Async delay to update UI |
| 6337 | 6348 | await delay(1); |
| @@ -6608,7 +6619,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | ||
| 6608 | 6619 | } |
| 6609 | 6620 | |
| 6610 | 6621 | avatarTemplate.attr('data-type', entity.type); |
| 6611 | 6622 | avatarTemplate.attr({ 'data-chid': id, 'id': `CharID${id}` }); |
| 6612 | 6623 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); |
| 6613 | 6624 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); |
| 6614 | 6625 | if (highlightFavs) { |
| @@ -6623,8 +6634,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | ||
| 6623 | 6634 | avatarTemplate.addClass(grpTemplate.attr('class')); |
| 6624 | 6635 | avatarTemplate.empty(); |
| 6625 | 6636 | avatarTemplate.append(grpTemplate.children()); |
| 6637 | + avatarTemplate.attr({ 'data-grid': id, 'data-chid': null }); | |
| 6626 | 6638 | avatarTemplate.attr('title', `[Group] ${entity.item.name}`); |
| 6627 | 6639 | } |
| 6640 | + else if (entity.type === 'persona') { | |
| 6641 | + avatarTemplate.attr({ 'data-pid': id, 'data-chid': null }); | |
| 6642 | + avatarTemplate.find('img').attr('src', getUserAvatar(entity.item.avatar)); | |
| 6643 | + avatarTemplate.attr('title', `[Persona] ${entity.item.name}\nFile: ${entity.item.avatar}`); | |
| 6644 | + } | |
| 6628 | 6645 | |
| 6629 | 6646 | if (interactable) { |
| 6630 | 6647 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); |
| @@ -6861,14 +6878,14 @@ export function changeMainAPI() { | ||
| 6861 | 6878 | forceCharacterEditorTokenize(); |
| 6862 | 6879 | } |
| 6863 | 6880 | |
| 6864 | 6881 | export function setUserName(value, { toastPersonaNameChange = true } = {}) { |
| 6865 | 6882 | name1 = value; |
| 6866 | 6883 | if (name1 === undefined || name1 == '') |
| 6867 | 6884 | name1 = default_user_name; |
| 6868 | 6885 | console.log(`User name changed to ${name1}`); |
| 6869 | 6886 | $('#your_name').valtext(name1); |
| 6870 | 6887 | if (toastPersonaNameChange && power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 6871 | 6888 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Current personaPersona updatedChanged`); |
| 6872 | 6889 | } |
| 6873 | 6890 | saveSettingsDebounced(); |
| 6874 | 6891 | } |
| @@ -6920,7 +6937,7 @@ export async function getSettings() { | ||
| 6920 | 6937 | settings = JSON.parse(data.settings); |
| 6921 | 6938 | if (settings.username !== undefined && settings.username !== '') { |
| 6922 | 6939 | name1 = settings.username; |
| 6923 | 6940 | $('#your_name').valtext(name1); |
| 6924 | 6941 | } |
| 6925 | 6942 | |
| 6926 | 6943 | accountStorage.init(settings?.accountStorage); |
| @@ -10054,7 +10071,7 @@ jQuery(async function () { | ||
| 10054 | 10071 | }); |
| 10055 | 10072 | |
| 10056 | 10073 | $(document).on('click', '.character_select', async function () { |
| 10057 | 10074 | const id = Number($(this).attr('data-chid')); |
| 10058 | 10075 | await selectCharacterById(id); |
| 10059 | 10076 | }); |
| 10060 | 10077 | |
| @@ -672,7 +672,7 @@ class BulkEditOverlay { | ||
| 672 | 672 | * @param {HTMLElement} currentCharacter - The html element of the currently toggled character |
| 673 | 673 | */ |
| 674 | 674 | handleShiftClick = (currentCharacter) => { |
| 675 | 675 | const characterId = Number(currentCharacter.getAttribute('data-chid')); |
| 676 | 676 | const select = !this.selectedCharacters.includes(characterId); |
| 677 | 677 | |
| 678 | 678 | if (this.lastSelected.characterId && this.lastSelected.select !== undefined) { |
| @@ -691,7 +691,7 @@ class BulkEditOverlay { | ||
| 691 | 691 | * @param {boolean} [param1.markState] - Whether the toggle of this character should be remembered as the last done toggle |
| 692 | 692 | */ |
| 693 | 693 | toggleSingleCharacter = (character, { markState = true } = {}) => { |
| 694 | 694 | const characterId = Number(character.getAttribute('data-chid')); |
| 695 | 695 | |
| 696 | 696 | const select = !this.selectedCharacters.includes(characterId); |
| 697 | 697 | const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass); |
| @@ -699,11 +699,11 @@ class BulkEditOverlay { | ||
| 699 | 699 | if (select) { |
| 700 | 700 | character.classList.add(BulkEditOverlay.selectedClass); |
| 701 | 701 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; |
| 702 | 702 | this.#selectedCharacters.push(String(characterId)); |
| 703 | 703 | } else { |
| 704 | 704 | character.classList.remove(BulkEditOverlay.selectedClass); |
| 705 | 705 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; |
| 706 | 706 | this.#selectedCharacters = this.#selectedCharacters.filter(item => String(characterId) !== item); |
| 707 | 707 | } |
| 708 | 708 | |
| 709 | 709 | this.updateSelectedCount(); |
| @@ -732,15 +732,15 @@ class BulkEditOverlay { | ||
| 732 | 732 | * @param {boolean} select - <c>true</c> if the characters in the range are to be selected, <c>false</c> if deselected |
| 733 | 733 | */ |
| 734 | 734 | toggleCharactersInRange = (currentCharacter, select) => { |
| 735 | 735 | const currentCharacterId = Number(currentCharacter.getAttribute('data-chid')); |
| 736 | 736 | const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); |
| 737 | 737 | |
| 738 | 738 | const startIndex = characters.findIndex(c => Number(c.getAttribute('data-chid')) === Number(this.lastSelected.characterId)); |
| 739 | 739 | const endIndex = characters.findIndex(c => Number(c.getAttribute('data-chid')) === currentCharacterId); |
| 740 | 740 | |
| 741 | 741 | for (let i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) { |
| 742 | 742 | const character = characters[i]; |
| 743 | 743 | const characterId = Number(character.getAttribute('data-chid')); |
| 744 | 744 | const isCharacterSelected = this.selectedCharacters.includes(characterId); |
| 745 | 745 | |
| 746 | 746 | // Only toggle the character if it wasn't on the state we have are toggling towards. |
| @@ -281,7 +281,7 @@ async function RA_autoloadchat() { | ||
| 281 | 281 | if (active_character !== null && active_character !== undefined) { |
| 282 | 282 | const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); |
| 283 | 283 | if (active_character_id !== -1) { |
| 284 | 284 | await selectCharacterById(String(active_character_id)); |
| 285 | 285 | |
| 286 | 286 | // Do a little tomfoolery to spoof the tag selector |
| 287 | 287 | const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); |
| @@ -894,14 +894,14 @@ export function initRossMods() { | ||
| 894 | 894 | |
| 895 | 895 | // when a char is selected from the list, save their name as the auto-load character for next page load |
| 896 | 896 | $(document).on('click', '.character_select', function () { |
| 897 | 897 | const characterId = $(this).attr('chid') || $(this).data('id-chid'); |
| 898 | 898 | setActiveCharacter(characterId); |
| 899 | 899 | setActiveGroup(null); |
| 900 | 900 | saveSettingsDebounced(); |
| 901 | 901 | }); |
| 902 | 902 | |
| 903 | 903 | $(document).on('click', '.group_select', function () { |
| 904 | 904 | const groupId = $(this).attr('data-chid') || $(this).attr('grid') || $(this).data('id-grid'); |
| 905 | 905 | setActiveCharacter(null); |
| 906 | 906 | setActiveGroup(groupId); |
| 907 | 907 | saveSettingsDebounced(); |
| @@ -691,7 +691,7 @@ export function getGroupBlock(group) { | ||
| 691 | 691 | |
| 692 | 692 | const template = $('#group_list_template .group_select').clone(); |
| 693 | 693 | template.data('id', group.id); |
| 694 | 694 | template.attr('data-grid', group.id); |
| 695 | 695 | template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`); |
| 696 | 696 | template.find('.group_fav_icon').css('display', 'none'); |
| 697 | 697 | template.addClass(group.fav ? 'is_fav' : ''); |
| @@ -1359,7 +1359,7 @@ function getGroupCharacterBlock(character) { | ||
| 1359 | 1359 | template.data('id', character.avatar); |
| 1360 | 1360 | template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar }); |
| 1361 | 1361 | template.find('.ch_name').text(character.name); |
| 1362 | 1362 | template.attr('data-chid', characters.indexOf(character)); |
| 1363 | 1363 | template.find('.ch_fav').val(isFav); |
| 1364 | 1364 | template.toggleClass('is_fav', isFav); |
| 1365 | 1365 | |
| @@ -1645,7 +1645,7 @@ async function onGroupActionClick(event) { | ||
| 1645 | 1645 | } |
| 1646 | 1646 | |
| 1647 | 1647 | if (action === 'speak') { |
| 1648 | 1648 | const chid = Number(member.attr('data-chid')); |
| 1649 | 1649 | if (Number.isInteger(chid)) { |
| 1650 | 1650 | Generate('normal', { force_chid: chid }); |
| 1651 | 1651 | } |
| @@ -1700,7 +1700,7 @@ function openCharacterDefinition(characterSelect) { | ||
| 1700 | 1700 | return; |
| 1701 | 1701 | } |
| 1702 | 1702 | |
| 1703 | 1703 | const chid = characterSelect.attr('data-chid'); |
| 1704 | 1704 | |
| 1705 | 1705 | if (chid === null || chid === undefined) { |
| 1706 | 1706 | return; |
| @@ -2018,7 +2018,7 @@ jQuery(() => { | ||
| 2018 | 2018 | } |
| 2019 | 2019 | |
| 2020 | 2020 | $(document).on('click', '.group_select', function () { |
| 2021 | 2021 | const groupId = $(this).attr('data-chid') || $(this).attr('grid') || $(this).data('id-grid'); |
| 2022 | 2022 | openGroupById(groupId); |
| 2023 | 2023 | }); |
| 2024 | 2024 | $('#rm_group_filter').on('input', filterGroupMembers); |
| @@ -1,4 +1,6 @@ | ||
| 1 | 1 | import { |
| 2 | + buildAvatarList, | |
| 3 | + characterToEntity, | |
| 2 | 4 | characters, |
| 3 | 5 | chat, |
| 4 | 6 | chat_metadata, |
| @@ -7,7 +9,10 @@ import { | ||
| 7 | 9 | event_types, |
| 8 | 10 | getRequestHeaders, |
| 9 | 11 | getThumbnailUrl, |
| 12 | + groupToEntity, | |
| 13 | + menu_type, | |
| 10 | 14 | name1, |
| 15 | + name2, | |
| 11 | 16 | reloadCurrentChat, |
| 12 | 17 | saveChatConditional, |
| 13 | 18 | saveMetadata, |
| @@ -17,23 +22,63 @@ import { | ||
| 17 | 22 | } from '../script.js'; |
| 18 | 23 | import { persona_description_positions, power_user } from './power-user.js'; |
| 19 | 24 | import { getTokenCountAsync } from './tokenizers.js'; |
| 20 | 25 | import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js'; |
| 21 | 26 | import { debounce_timeout } from './constants.js'; |
| 22 | 27 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 23 | 28 | import { groups, selected_group } from './group-chats.js'; |
| 24 | 29 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 25 | 30 | import { t } from './i18n.js'; |
| 26 | 31 | import { openWorldInfoEditor, world_names } from './world-info.js'; |
| 27 | 32 | import { renderTemplateAsync } from './templates.js'; |
| 33 | +import { saveMetadataDebounced } from './extensions.js'; | |
| 28 | 34 | import { accountStorage } from './util/AccountStorage.js'; |
| 35 | +import { SlashCommand } from './slash-commands/SlashCommand.js'; | |
| 36 | +import { SlashCommandNamedArgument, ARGUMENT_TYPE, SlashCommandArgument } from './slash-commands/SlashCommandArgument.js'; | |
| 37 | +import { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js'; | |
| 38 | +import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js'; | |
| 39 | +import { SlashCommandParser } from './slash-commands/SlashCommandParser.js'; | |
| 40 | + | |
| 41 | +/** | |
| 42 | + * @typedef {object} PersonaConnection A connection between a character and a character or group entity | |
| 43 | + * @property {'character' | 'group'} type - Type of connection | |
| 44 | + * @property {string} id - ID of the connection (character key (avatar url), group id) | |
| 45 | + */ | |
| 46 | + | |
| 47 | +/** @typedef {'chat' | 'character' | 'default'} PersonaLockType Type of the persona lock */ | |
| 48 | + | |
| 49 | +/** | |
| 50 | + * @typedef {object} PersonaState | |
| 51 | + * @property {string} avatarId - The avatar id of the persona | |
| 52 | + * @property {boolean} default - Whether this persona is the default one for all new chats | |
| 53 | + * @property {object} locked - An object containing the lock states | |
| 54 | + * @property {boolean} locked.chat - Whether the persona is locked to the currently open chat | |
| 55 | + * @property {boolean} locked.character - Whether the persona is locked to the currently open character or group | |
| 56 | + */ | |
| 57 | + | |
| 58 | +const USER_AVATAR_PATH = 'User Avatars/'; | |
| 29 | 59 | |
| 30 | 60 | let savePersonasPage = 0; |
| 31 | 61 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| 32 | 62 | const DEFAULT_DEPTH = 2; |
| 33 | 63 | const DEFAULT_ROLE = 0; |
| 64 | + | |
| 65 | +/** @type {string} The currently selected persona (identified by its avatar) */ | |
| 34 | 66 | export let user_avatar = ''; |
| 67 | + | |
| 68 | +/** @type {FilterHelper} Filter helper for the persona list */ | |
| 35 | 69 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); |
| 36 | 70 | |
| 71 | +/** @type {function(string): void} */ | |
| 72 | +let navigateToAvatar = () => { }; | |
| 73 | + | |
| 74 | +/** | |
| 75 | + * Checks if the Persona Management panel is currently open | |
| 76 | + * @returns {boolean} | |
| 77 | + */ | |
| 78 | +export function isPersonaPanelOpen() { | |
| 79 | + return document.querySelector('#persona-management-button .drawer-content')?.classList.contains('openDrawer') ?? false; | |
| 80 | +} | |
| 81 | + | |
| 37 | 82 | function switchPersonaGridView() { |
| 38 | 83 | const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 39 | 84 | $('#user_avatar_block').toggleClass('gridView', state); |
| @@ -45,33 +90,31 @@ function switchPersonaGridView() { | ||
| 45 | 90 | * @returns {string} User avatar URL |
| 46 | 91 | */ |
| 47 | 92 | export function getUserAvatar(avatarImg) { |
| 48 | 93 | return `User Avatars/${USER_AVATAR_PATH}${avatarImg}`; |
| 49 | 94 | } |
| 50 | 95 | |
| 51 | 96 | export function initUserAvatar(avatar) { |
| 52 | 97 | user_avatar = avatar; |
| 53 | 98 | reloadUserAvatar(); |
| 54 | 99 | highlightSelectedAvatarupdatePersonaUIStates(); |
| 55 | 100 | } |
| 56 | 101 | |
| 57 | 102 | /** |
| 58 | 103 | * Sets a user avatar file |
| 59 | 104 | * @param {string} imgfile Link to an image file |
| 105 | + * @param {object} [options] Optional settings | |
| 106 | + * @param {boolean} [options.toastPersonaNameChange=true] Whether to show a toast when the persona name is changed | |
| 107 | + * @param {boolean} [options.navigateToCurrent=false] Whether to navigate to the current persona after setting the avatar | |
| 60 | 108 | */ |
| 61 | -export function setUserAvatar(imgfile) { | |
| 109 | +export function setUserAvatar(imgfile, { toastPersonaNameChange = true, navigateToCurrent = false } = {}) { | |
| 62 | 110 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfiledata-avatar-id'); |
| 63 | 111 | reloadUserAvatar(); |
| 64 | - highlightSelectedAvatar(); | |
| 112 | + updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); | |
| 65 | 113 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 66 | 114 | saveSettingsDebounced(); |
| 67 | 115 | $('.zoomed_avatar[forchar]').remove(); |
| 68 | 116 | } |
| 69 | 117 | |
| 70 | -function highlightSelectedAvatar() { | |
| 71 | - $('#user_avatar_block .avatar-container').removeClass('selected'); | |
| 72 | - $(`#user_avatar_block .avatar-container[imgfile="${user_avatar}"]`).addClass('selected'); | |
| 73 | -} | |
| 74 | - | |
| 75 | 118 | function reloadUserAvatar(force = false) { |
| 76 | 119 | $('.mes').each(function () { |
| 77 | 120 | const avatarImg = $(this).find('.avatar img'); |
| @@ -131,29 +174,49 @@ function verifyPersonaSearchSortRule() { | ||
| 131 | 174 | |
| 132 | 175 | /** |
| 133 | 176 | * Gets a rendered avatar block. |
| 134 | 177 | * @param {string} nameavatarId Avatar file name |
| 135 | 178 | * @returns {JQuery<HTMLElement>} Avatar block |
| 136 | 179 | */ |
| 137 | 180 | function getUserAvatarBlock(nameavatarId) { |
| 138 | 181 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; |
| 139 | 182 | const template = $('#user_avatar_template .avatar-container').clone(); |
| 140 | 183 | const personaName = power_user.personas[nameavatarId]; |
| 141 | 184 | const personaDescription = power_user.persona_descriptions[nameavatarId]?.description; |
| 185 | + | |
| 142 | 186 | template.find('.ch_name').text(personaName || '[Unnamed Persona]'); |
| 143 | 187 | template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); |
| 144 | 188 | template.attr('imgfiledata-avatar-id', nameavatarId); |
| 145 | 189 | template.find('.avatar').attr('imgfiledata-avatar-id', nameavatarId).attr('title', nameavatarId); |
| 146 | 190 | template.toggleClass('default_persona', nameavatarId === power_user.default_persona); |
| 147 | 191 | let avatarUrl = getUserAvatar(nameavatarId); |
| 148 | 192 | if (isFirefox) { |
| 149 | 193 | avatarUrl += '?t=' + Date.now(); |
| 150 | 194 | } |
| 151 | 195 | template.find('img').attr('src', avatarUrl); |
| 196 | + | |
| 197 | + // Make sure description block has at least three rows. Otherwise height looks inconsistent. I don't have a better idea for this. | |
| 198 | + const currentText = template.find('.ch_description').text(); | |
| 199 | + if (currentText.split('\n').length < 3) { | |
| 200 | + template.find('.ch_description').text(currentText + '\n\xa0\n\xa0'); | |
| 201 | + } | |
| 202 | + | |
| 152 | 203 | $('#user_avatar_block').append(template); |
| 153 | 204 | return template; |
| 154 | 205 | } |
| 155 | 206 | |
| 156 | 207 | /** |
| 208 | + * Initialize missing personas in the power user settings. | |
| 209 | + * @param {string[]} avatarsList List of avatar file names | |
| 210 | + */ | |
| 211 | +function addMissingPersonas(avatarsList) { | |
| 212 | + for (const persona of avatarsList) { | |
| 213 | + if (!power_user.personas[persona]) { | |
| 214 | + initPersona(persona, '[Unnamed Persona]', ''); | |
| 215 | + } | |
| 216 | + } | |
| 217 | +} | |
| 218 | + | |
| 219 | +/** | |
| 157 | 220 | * Gets a list of user avatars. |
| 158 | 221 | * @param {boolean} doRender Whether to render the list |
| 159 | 222 | * @param {string} openPageAt Item to be opened at |
| @@ -175,6 +238,8 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 175 | 238 | return allEntities; |
| 176 | 239 | } |
| 177 | 240 | |
| 241 | + // If any persona is missing from the power user settings, we add it | |
| 242 | + addMissingPersonas(allEntities); | |
| 178 | 243 | // Before printing the personas, we check if we should enable/disable search sorting |
| 179 | 244 | verifyPersonaSearchSortRule(); |
| 180 | 245 | |
| @@ -203,7 +268,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 203 | 268 | for (const item of data) { |
| 204 | 269 | $(listId).append(getUserAvatarBlock(item)); |
| 205 | 270 | } |
| 206 | 271 | highlightSelectedAvatarupdatePersonaUIStates(); |
| 207 | 272 | }, |
| 208 | 273 | afterSizeSelectorChange: function (e) { |
| 209 | 274 | accountStorage.setItem(storageKey, e.target.value); |
| @@ -216,14 +281,16 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 216 | 281 | }, |
| 217 | 282 | }); |
| 218 | 283 | |
| 219 | 284 | ifnavigateToAvatar = (openPageAtavatarId) => { |
| 220 | 285 | const avatarIndex = entities.indexOf(openPageAtavatarId); |
| 221 | 286 | const page = Math.floor(avatarIndex / perPage) + 1; |
| 222 | 287 | |
| 223 | 288 | if (avatarIndex !== -1) { |
| 224 | 289 | $('#persona_pagination_container').pagination('go', page); |
| 225 | 290 | } |
| 226 | 291 | }; |
| 292 | + | |
| 293 | + openPageAt && navigateToAvatar(openPageAt); | |
| 227 | 294 | |
| 228 | 295 | return allEntities; |
| 229 | 296 | } |
| @@ -233,7 +300,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | ||
| 233 | 300 | * Uploads an avatar file to the server |
| 234 | 301 | * @param {string} url URL for the avatar file |
| 235 | 302 | * @param {string} [name] Optional name for the avatar file |
| 236 | 303 | * @returns {Promise} Promise objectthat representingresolves when the AJAXavatar requestis uploaded |
| 237 | 304 | */ |
| 238 | 305 | async function uploadUserAvatar(url, name) { |
| 239 | 306 | const fetchResult = await fetch(url); |
| @@ -246,18 +313,17 @@ async function uploadUserAvatar(url, name) { | ||
| 246 | 313 | formData.append('overwrite_name', name); |
| 247 | 314 | } |
| 248 | 315 | |
| 249 | - return jQuery.ajax({ | |
| 316 | + const headers = getRequestHeaders(); | |
| 250 | - type: 'POST', | |
| 317 | + delete headers['Content-Type']; | |
| 251 | - url: '/api/avatars/upload', | |
| 318 | + | |
| 252 | - data: formData, | |
| 319 | + await fetch('/api/avatars/upload', { | |
| 253 | - beforeSend: () => { }, | |
| 320 | + method: 'POST', | |
| 254 | 321 | cacheheaders: falseheaders, |
| 255 | 322 | contentTypecache: false'no-cache', |
| 256 | 323 | processDatabody: falseformData, |
| 257 | - success: async function () { | |
| 258 | - await getUserAvatars(true, name); | |
| 259 | - }, | |
| 260 | 324 | }); |
| 325 | + | |
| 326 | + await getUserAvatars(true, name); | |
| 261 | 327 | } |
| 262 | 328 | |
| 263 | 329 | async function changeUserAvatar(e) { |
| @@ -298,33 +364,35 @@ async function changeUserAvatar(e) { | ||
| 298 | 364 | formData.set('avatar', convertedFile); |
| 299 | 365 | } |
| 300 | 366 | |
| 301 | - jQuery.ajax({ | |
| 367 | + const headers = getRequestHeaders(); | |
| 302 | - type: 'POST', | |
| 368 | + delete headers['Content-Type']; | |
| 303 | - url: url, | |
| 304 | - data: formData, | |
| 305 | - beforeSend: () => { }, | |
| 306 | - cache: false, | |
| 307 | - contentType: false, | |
| 308 | - processData: false, | |
| 309 | - success: async function (data) { | |
| 310 | - // If the user uploaded a new avatar, we want to make sure it's not cached | |
| 311 | - const name = formData.get('overwrite_name'); | |
| 312 | - if (name) { | |
| 313 | - await fetch(getUserAvatar(String(name)), { cache: 'no-cache' }); | |
| 314 | - reloadUserAvatar(true); | |
| 315 | - } | |
| 316 | - | |
| 317 | - if (!name && data.path) { | |
| 318 | - await getUserAvatars(); | |
| 319 | - await delay(500); | |
| 320 | - await createPersona(data.path); | |
| 321 | - } | |
| 322 | 369 | |
| 323 | - await getUserAvatars(true, name || data.path); | |
| 370 | + const response = await fetch(url, { | |
| 324 | - }, | |
| 371 | + method: 'POST', | |
| 325 | - error: (jqXHR, exception) => { }, | |
| 372 | + headers: headers, | |
| 373 | + cache: 'no-cache', | |
| 374 | + body: formData, | |
| 326 | 375 | }); |
| 327 | 376 | |
| 377 | + if (response.ok) { | |
| 378 | + const data = await response.json(); | |
| 379 | + | |
| 380 | + // If the user uploaded a new avatar, we want to make sure it's not cached | |
| 381 | + const name = formData.get('overwrite_name'); | |
| 382 | + if (name) { | |
| 383 | + await fetch(getUserAvatar(String(name)), { cache: 'no-cache' }); | |
| 384 | + reloadUserAvatar(true); | |
| 385 | + } | |
| 386 | + | |
| 387 | + if (!name && data.path) { | |
| 388 | + await getUserAvatars(); | |
| 389 | + await delay(500); | |
| 390 | + await createPersona(data.path); | |
| 391 | + } | |
| 392 | + | |
| 393 | + await getUserAvatars(true, name || data.path); | |
| 394 | + } | |
| 395 | + | |
| 328 | 396 | // Will allow to select the same file twice in a row |
| 329 | 397 | form.reset(); |
| 330 | 398 | } |
| @@ -384,13 +452,25 @@ export function initPersona(avatarId, personaName, personaDescription) { | ||
| 384 | 452 | saveSettingsDebounced(); |
| 385 | 453 | } |
| 386 | 454 | |
| 455 | +/** | |
| 456 | + * Converts a character given character (either by character id or the current character) to a persona. | |
| 457 | + * | |
| 458 | + * If a persona with the same name already exists, the user is prompted to confirm whether or not to overwrite it. | |
| 459 | + * If the character description contains {{char}} or {{user}} macros, the user is prompted to confirm whether or not to swap them for persona macros. | |
| 460 | + * | |
| 461 | + * The function creates a new persona with the same name as the character, and sets the persona description to the character description with the macros swapped. | |
| 462 | + * The function also saves the settings and refreshes the persona selector. | |
| 463 | + * | |
| 464 | + * @param {number} [characterId] - The ID of the character to convert to a persona. Defaults to the current character ID. | |
| 465 | + * @returns {Promise<boolean>} A promise that resolves to true if the character was converted, false otherwise. | |
| 466 | + */ | |
| 387 | 467 | export async function convertCharacterToPersona(characterId = null) { |
| 388 | 468 | if (null === characterId) characterId = this_chid; |
| 389 | 469 | |
| 390 | 470 | const avatarUrl = characters[characterId]?.avatar; |
| 391 | 471 | if (!avatarUrl) { |
| 392 | 472 | console.log('No avatar found for this character'); |
| 393 | 473 | return false; |
| 394 | 474 | } |
| 395 | 475 | |
| 396 | 476 | const name = characters[characterId]?.name; |
| @@ -401,7 +481,7 @@ export async function convertCharacterToPersona(characterId = null) { | ||
| 401 | 481 | const confirm = await Popup.show.confirm(t`Overwrite Existing Persona`, t`This character exists as a persona already. Do you want to overwrite it?`); |
| 402 | 482 | if (!confirm) { |
| 403 | 483 | console.log('User cancelled the overwrite of the persona'); |
| 404 | 484 | return false; |
| 405 | 485 | } |
| 406 | 486 | } |
| 407 | 487 | |
| @@ -439,6 +519,7 @@ export async function convertCharacterToPersona(characterId = null) { | ||
| 439 | 519 | await getUserAvatars(true, overwriteName); |
| 440 | 520 | // Reload the persona description |
| 441 | 521 | setPersonaDescription(); |
| 522 | + return true; | |
| 442 | 523 | } |
| 443 | 524 | |
| 444 | 525 | /** |
| @@ -450,7 +531,12 @@ const countPersonaDescriptionTokens = debounce(async () => { | ||
| 450 | 531 | $('#persona_description_token_count').text(String(count)); |
| 451 | 532 | }, debounce_timeout.relaxed); |
| 452 | 533 | |
| 534 | +/** | |
| 535 | + * Updates the UI for the Persona Management page with the current persona values | |
| 536 | + */ | |
| 453 | 537 | export function setPersonaDescription() { |
| 538 | + $('#your_name').text(name1); | |
| 539 | + | |
| 454 | 540 | if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) { |
| 455 | 541 | power_user.persona_description_position = persona_description_positions.IN_PROMPT; |
| 456 | 542 | } |
| @@ -468,116 +554,219 @@ export function setPersonaDescription() { | ||
| 468 | 554 | .prop('selected', String(true)); |
| 469 | 555 | $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); |
| 470 | 556 | countPersonaDescriptionTokens(); |
| 557 | + | |
| 558 | + updatePersonaUIStates(); | |
| 559 | + updatePersonaConnectionsAvatarList(); | |
| 471 | 560 | } |
| 472 | 561 | |
| 473 | -export function autoSelectPersona(name) { | |
| 562 | +/** | |
| 474 | - for (const [key, value] of Object.entries(power_user.personas)) { | |
| 563 | + * Gets a list of all personas in the current chat. | |
| 475 | - if (value === name) { | |
| 564 | + * | |
| 476 | - console.log(`Auto-selecting persona ${key} for name ${name}`); | |
| 565 | + * @returns {string[]} An array of persona identifiers | |
| 477 | - setUserAvatar(key); | |
| 566 | + */ | |
| 478 | - return; | |
| 567 | +function getPersonasOfCurrentChat() { | |
| 479 | - } | |
| 568 | + const personas = chat.filter(message => String(message.force_avatar).startsWith(USER_AVATAR_PATH)) | |
| 480 | - } | |
| 569 | + .map(message => message.force_avatar.replace(USER_AVATAR_PATH, '')) | |
| 570 | + .filter(onlyUnique); | |
| 571 | + return personas; | |
| 481 | 572 | } |
| 482 | 573 | |
| 483 | 574 | /** |
| 484 | 575 | * UpdatesBuilds thea namelist of a persona ifavatars itand existspopulates the given block element with them. |
| 485 | - * @param {string} avatarId User avatar id | |
| 576 | + * | |
| 486 | 577 | * @param {stringHTMLElement} newNameblock New- nameThe forHTML element where the personaavatar list will be rendered |
| 578 | + * @param {string[]} personas - An array of persona identifiers | |
| 579 | + * @param {Object} [options] - Optional settings for building the avatar list | |
| 580 | + * @param {boolean} [options.empty=true] - Whether to clear the block element before adding avatars | |
| 581 | + * @param {boolean} [options.interactable=false] - Whether the avatars should be interactable | |
| 582 | + * @param {boolean} [options.highlightFavs=true] - Whether to highlight favorite avatars | |
| 487 | 583 | */ |
| 488 | -async function updatePersonaNameIfExists(avatarId, newName) { | |
| 584 | +export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) { | |
| 489 | - if (avatarId in power_user.personas) { | |
| 585 | + const personaEntities = personas.map(avatar => ({ | |
| 490 | - power_user.personas[avatarId] = newName; | |
| 586 | + type: 'persona', | |
| 491 | - console.log(`Updated persona name for ${avatarId} to ${newName}`); | |
| 587 | + id: avatar, | |
| 492 | - } else { | |
| 588 | + item: { | |
| 493 | - power_user.personas[avatarId] = newName; | |
| 589 | + name: power_user.personas[avatar], | |
| 494 | - power_user.persona_descriptions[avatarId] = { | |
| 590 | + description: power_user.persona_descriptions[avatar]?.description || '', | |
| 495 | 591 | descriptionavatar: ''avatar, |
| 496 | - position: persona_description_positions.IN_PROMPT, | |
| 592 | + fav: power_user.default_persona === avatar, | |
| 497 | - depth: DEFAULT_DEPTH, | |
| 593 | + }, | |
| 498 | - role: DEFAULT_ROLE, | |
| 594 | + })); | |
| 499 | - lorebook: '', | |
| 500 | - }; | |
| 501 | - console.log(`Created persona name for ${avatarId} as ${newName}`); | |
| 502 | - } | |
| 503 | 595 | |
| 504 | - await getUserAvatars(true, avatarId); | |
| 596 | + buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs }); | |
| 505 | - saveSettingsDebounced(); | |
| 506 | 597 | } |
| 507 | 598 | |
| 508 | -async function bindUserNameToPersona(e) { | |
| 599 | +/** | |
| 509 | - e?.stopPropagation(); | |
| 600 | + * Displays avatar connections for the current persona. | |
| 510 | - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | |
| 601 | + * Converts connections to entities and populates the avatar list. Shows a message if no connections are found. | |
| 602 | + */ | |
| 603 | +export function updatePersonaConnectionsAvatarList() { | |
| 604 | + /** @type {PersonaConnection[]} */ | |
| 605 | + const connections = power_user.persona_descriptions[user_avatar]?.connections ?? []; | |
| 606 | + const entities = connections.map(connection => { | |
| 607 | + if (connection.type === 'character') { | |
| 608 | + const character = characters.find(c => c.avatar === connection.id); | |
| 609 | + if (character) return characterToEntity(character, getCharIndex(character)); | |
| 610 | + } | |
| 611 | + if (connection.type === 'group') { | |
| 612 | + const group = groups.find(g => g.id === connection.id); | |
| 613 | + if (group) return groupToEntity(group); | |
| 614 | + } | |
| 615 | + return undefined; | |
| 616 | + }).filter(entity => entity?.item !== undefined); | |
| 511 | 617 | |
| 512 | 618 | if (!avatarIdentities.length) { |
| 513 | - console.warn('No avatar id found'); | |
| 619 | + buildAvatarList($('#persona_connections_list'), entities, { interactable: true }); | |
| 514 | - return; | |
| 620 | + else | |
| 515 | - } | |
| 621 | + $('#persona_connections_list').text(t`[No character connections. Click one of the buttons above to connect this persona.]`); | |
| 622 | +} | |
| 516 | 623 | |
| 517 | - let personaUnbind = false; | |
| 518 | - const existingPersona = power_user.personas[avatarId]; | |
| 519 | - const personaName = await Popup.show.input( | |
| 520 | - t`Enter a name for this persona:`, | |
| 521 | - t`(If empty name is provided, this will unbind the name from this avatar)`, | |
| 522 | - existingPersona || '', | |
| 523 | - { onClose: (p) => { personaUnbind = p.value === '' && p.result === POPUP_RESULT.AFFIRMATIVE; } }); | |
| 524 | 624 | |
| 525 | - // If the user clicked cancel, don't do anything | |
| 625 | +/** | |
| 526 | - if (personaName === null && !personaUnbind) { | |
| 626 | + * Displays a popup for persona selection and returns the selected persona. | |
| 527 | - return; | |
| 627 | + * | |
| 528 | - } | |
| 628 | + * @param {string} title - The title to display in the popup | |
| 629 | + * @param {string} text - The text to display in the popup | |
| 630 | + * @param {string[]} personas - An array of persona ids to display for selection | |
| 631 | + * @param {Object} [options] - Optional settings for the popup | |
| 632 | + * @param {string} [options.okButton='None'] - The label for the OK button | |
| 633 | + * @param {(element: HTMLElement, ev: MouseEvent) => any} [options.shiftClickHandler] - A function to handle shift-click | |
| 634 | + * @param {boolean|string[]} [options.highlightPersonas=false] - Whether to highlight personas - either by providing a list of persona keys, or true to highlight all present in current chat | |
| 635 | + * @param {PersonaConnection} [options.targetedChar] - The targeted character or gorup for this persona selection | |
| 636 | + * @returns {Promise<string?>} - A promise that resolves to the selected persona id or null if no selection was made | |
| 637 | + */ | |
| 638 | +export async function askForPersonaSelection(title, text, personas, { okButton = 'None', shiftClickHandler = undefined, highlightPersonas = false, targetedChar = undefined } = {}) { | |
| 639 | + const content = document.createElement('div'); | |
| 640 | + const titleElement = document.createElement('h3'); | |
| 641 | + titleElement.textContent = title; | |
| 642 | + content.appendChild(titleElement); | |
| 643 | + | |
| 644 | + const textElement = document.createElement('div'); | |
| 645 | + textElement.classList.add('multiline', 'm-b-1'); | |
| 646 | + textElement.textContent = text; | |
| 647 | + content.appendChild(textElement); | |
| 648 | + | |
| 649 | + const personaListBlock = document.createElement('div'); | |
| 650 | + personaListBlock.classList.add('persona-list', 'avatars_inline', 'avatars_multiline', 'text_muted'); | |
| 651 | + content.appendChild(personaListBlock); | |
| 652 | + | |
| 653 | + if (personas.length > 0) | |
| 654 | + buildPersonaAvatarList(personaListBlock, personas, { interactable: true }); | |
| 655 | + else | |
| 656 | + personaListBlock.textContent = t`[Currently no personas connected]`; | |
| 657 | + | |
| 658 | + const personasToHighlight = highlightPersonas instanceof Array ? highlightPersonas : (highlightPersonas ? getPersonasOfCurrentChat() : []); | |
| 659 | + | |
| 660 | + // Make the persona blocks clickable and close the popup | |
| 661 | + personaListBlock.querySelectorAll('.avatar[data-type="persona"]').forEach(block => { | |
| 662 | + if (!(block instanceof HTMLElement)) return; | |
| 663 | + block.dataset.result = String(100 + personas.indexOf(block.dataset.pid)); | |
| 664 | + | |
| 665 | + if (shiftClickHandler) { | |
| 666 | + block.addEventListener('click', function (ev) { | |
| 667 | + if (ev.shiftKey) { | |
| 668 | + shiftClickHandler(this, ev); | |
| 669 | + } | |
| 670 | + }); | |
| 671 | + } | |
| 529 | 672 | |
| 530 | 673 | if (personaNamepersonasToHighlight && personaNamepersonasToHighlight.length > 0includes(block.dataset.pid)) { |
| 531 | - // If the user clicked ok and entered a name, bind the name to the persona | |
| 674 | + block.classList.add('is_active'); | |
| 532 | - console.log(`Binding persona ${avatarId} to name ${personaName}`); | |
| 675 | + block.title = block.title + '\n\n' + t`Was used in current chat.`; | |
| 533 | - power_user.personas[avatarId] = personaName; | |
| 676 | + if (block.classList.contains('is_fav')) block.title = block.title + '\n' + t`Is your default persona.`; | |
| 534 | - const descriptor = power_user.persona_descriptions[avatarId]; | |
| 535 | - const isCurrentPersona = avatarId === user_avatar; | |
| 536 | - | |
| 537 | - // Create a description object if it doesn't exist | |
| 538 | - if (!descriptor) { | |
| 539 | - // If the user is currently using this persona, set the description to the current description | |
| 540 | - power_user.persona_descriptions[avatarId] = { | |
| 541 | - description: isCurrentPersona ? power_user.persona_description : '', | |
| 542 | - position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.IN_PROMPT, | |
| 543 | - depth: isCurrentPersona ? power_user.persona_description_depth : DEFAULT_DEPTH, | |
| 544 | - role: isCurrentPersona ? power_user.persona_description_role : DEFAULT_ROLE, | |
| 545 | - lorebook: isCurrentPersona ? power_user.persona_description_lorebook : '', | |
| 546 | - }; | |
| 547 | 677 | } |
| 678 | + }); | |
| 548 | 679 | |
| 549 | - // If the user is currently using this persona, update the name | |
| 680 | + /** @type {import('./popup.js').CustomPopupButton[]} */ | |
| 550 | - if (isCurrentPersona) { | |
| 681 | + const customButtons = []; | |
| 551 | - console.log(`Auto-updating user name to ${personaName}`); | |
| 682 | + if (targetedChar) { | |
| 552 | - setUserName(personaName); | |
| 683 | + customButtons.push({ | |
| 684 | + text: t`Remove All Connections`, | |
| 685 | + result: 2, | |
| 686 | + action: () => { | |
| 687 | + for (const [personaId, description] of Object.entries(power_user.persona_descriptions)) { | |
| 688 | + /** @type {PersonaConnection[]} */ | |
| 689 | + const connections = description.connections; | |
| 690 | + if (connections) { | |
| 691 | + power_user.persona_descriptions[personaId].connections = connections.filter(c => { | |
| 692 | + if (targetedChar.type == c.type && targetedChar.id == c.id) return false; | |
| 693 | + return true; | |
| 694 | + }); | |
| 695 | + } | |
| 696 | + } | |
| 697 | + | |
| 698 | + saveSettingsDebounced(); | |
| 699 | + updatePersonaConnectionsAvatarList(); | |
| 700 | + if (power_user.persona_show_notifications) { | |
| 701 | + const name = targetedChar.type == 'character' ? characters[targetedChar.id]?.name : groups[targetedChar.id]?.name; | |
| 702 | + toastr.info(t`All connections to ${name} have been removed.`, t`Personas Unlocked`); | |
| 703 | + } | |
| 704 | + }, | |
| 705 | + }); | |
| 706 | + } | |
| 707 | + | |
| 708 | + const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton, customButtons: customButtons }); | |
| 709 | + const result = await popup.show(); | |
| 710 | + return Number(result) >= 100 ? personas[Number(result) - 100] : null; | |
| 711 | +} | |
| 712 | + | |
| 713 | +/** | |
| 714 | + * Automatically selects a persona based on the given name if a matching persona exists. | |
| 715 | + * @param {string} name - The name to search for | |
| 716 | + * @returns {boolean} True if a matching persona was found and selected, false otherwise | |
| 717 | + */ | |
| 718 | +export function autoSelectPersona(name) { | |
| 719 | + for (const [key, value] of Object.entries(power_user.personas)) { | |
| 720 | + if (value === name) { | |
| 721 | + console.log(`Auto-selecting persona ${key} for name ${name}`); | |
| 722 | + setUserAvatar(key); | |
| 723 | + return true; | |
| 553 | 724 | } |
| 554 | 725 | } else { |
| 555 | - // If the user clicked ok, but didn't enter a name, delete the persona | |
| 726 | + return false; | |
| 556 | - console.log(`Unbinding persona ${avatarId}`); | |
| 727 | +} | |
| 557 | - delete power_user.personas[avatarId]; | |
| 728 | + | |
| 558 | - delete power_user.persona_descriptions[avatarId]; | |
| 729 | +/** | |
| 730 | + * Renames the persona with the given avatar ID by showing a popup to enter a new name. | |
| 731 | + * @param {string} avatarId - ID of the avatar to rename | |
| 732 | + * @returns {Promise<boolean>} A promise that resolves to true if the persona was renamed, false otherwise | |
| 733 | + */ | |
| 734 | +async function renamePersona(avatarId) { | |
| 735 | + const currentName = power_user.personas[avatarId]; | |
| 736 | + const newName = await Popup.show.input(t`Rename Persona`, t`Enter a new name for this persona:`, currentName); | |
| 737 | + if (!newName || newName === currentName) { | |
| 738 | + console.debug('User cancelled renaming persona or name is unchanged'); | |
| 739 | + return false; | |
| 740 | + } | |
| 741 | + | |
| 742 | + power_user.personas[avatarId] = newName; | |
| 743 | + console.log(`Renamed persona ${avatarId} to ${newName}`); | |
| 744 | + | |
| 745 | + if (avatarId === user_avatar) { | |
| 746 | + setUserName(newName); | |
| 559 | 747 | } |
| 560 | 748 | |
| 561 | 749 | saveSettingsDebounced(); |
| 562 | 750 | await getUserAvatars(true, avatarId); |
| 751 | + updatePersonaUIStates(); | |
| 563 | 752 | setPersonaDescription(); |
| 753 | + return true; | |
| 564 | 754 | } |
| 565 | 755 | |
| 566 | -function selectCurrentPersona() { | |
| 756 | +/** | |
| 757 | + * Selects the persona with the currently set avatar ID by updating the user name and persona description, and updating the locked persona if the setting is enabled. | |
| 758 | + * @param {object} [options={}] - Optional settings | |
| 759 | + * @param {boolean} [options.toastPersonaNameChange=true] - Whether to show a toast when the persona name is changed | |
| 760 | + * @returns {Promise<void>} | |
| 761 | + */ | |
| 762 | +async function selectCurrentPersona({ toastPersonaNameChange = true } = {}) { | |
| 567 | 763 | const personaName = power_user.personas[user_avatar]; |
| 568 | 764 | if (personaName) { |
| 569 | 765 | const lockedPersonashouldAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; |
| 570 | - if (lockedPersona && lockedPersona !== user_avatar && power_user.persona_show_notifications) { | |
| 571 | - toastr.info( | |
| 572 | - t`To permanently set "${personaName}" as the selected persona, unlock and relock it using the "Lock" button. Otherwise, the selection resets upon reloading the chat.`, | |
| 573 | - t`This chat is locked to a different persona (${power_user.personas[lockedPersona]}).`, | |
| 574 | - { timeOut: 10000, extendedTimeOut: 20000, preventDuplicates: true }, | |
| 575 | - ); | |
| 576 | - } | |
| 577 | 766 | |
| 578 | 767 | if (personaName !== name1) { |
| 579 | 768 | console.log(`Auto-updating user name to ${personaName}`); |
| 580 | - setUserName(personaName); | |
| 769 | + setUserName(personaName, { toastPersonaNameChange: !shouldAutoLock && toastPersonaNameChange }); | |
| 581 | 770 | } |
| 582 | 771 | |
| 583 | 772 | const descriptor = power_user.persona_descriptions[user_avatar]; |
| @@ -600,67 +789,144 @@ function selectCurrentPersona() { | ||
| 600 | 789 | depth: DEFAULT_DEPTH, |
| 601 | 790 | role: DEFAULT_ROLE, |
| 602 | 791 | lorebook: '', |
| 792 | + connections: [], | |
| 603 | 793 | }; |
| 604 | 794 | } |
| 605 | 795 | |
| 606 | 796 | setPersonaDescription(); |
| 797 | + | |
| 798 | + // Update the locked persona if setting is enabled | |
| 799 | + if (shouldAutoLock) { | |
| 800 | + chat_metadata['persona'] = user_avatar; | |
| 801 | + console.log(`Auto locked persona to ${user_avatar}`); | |
| 802 | + if (toastPersonaNameChange && power_user.persona_show_notifications) { | |
| 803 | + toastr.success(`Persona ${personaName} selected and auto-locked to current chat`, t`Persona Selected`); | |
| 804 | + } | |
| 805 | + saveMetadataDebounced(); | |
| 806 | + updatePersonaUIStates(); | |
| 807 | + } | |
| 808 | + | |
| 809 | + // As the last step, inform user if the persona is only temporarily chosen | |
| 810 | + if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { | |
| 811 | + const temporary = getPersonaTemporaryLockInfo(); | |
| 812 | + if (temporary.isTemporary) { | |
| 813 | + toastr.info(t`This persona is only temporarily chosen. Click for more info.`, t`Temporary Persona`, { | |
| 814 | + preventDuplicates: true, onclick: () => { | |
| 815 | + toastr.info(temporary.info.replaceAll('\n', '<br />'), t`Temporary Persona`, { escapeHtml: false }); | |
| 816 | + }, | |
| 817 | + }); | |
| 818 | + } | |
| 819 | + } | |
| 607 | 820 | } |
| 608 | 821 | } |
| 609 | 822 | |
| 610 | 823 | /** |
| 611 | 824 | * Checks if thea personaconnection is locked for the current chat.character or group edit menu |
| 825 | + * @param {PersonaConnection} connection - Connection to check | |
| 826 | + * @returns {boolean} Whether the connection is locked | |
| 827 | + */ | |
| 828 | +export function isPersonaConnectionLocked(connection) { | |
| 829 | + return (!selected_group && connection.type === 'character' && connection.id === characters[this_chid]?.avatar) | |
| 830 | + || (selected_group && connection.type === 'group' && connection.id === selected_group); | |
| 831 | +} | |
| 832 | + | |
| 833 | +/** | |
| 834 | + * Checks if the persona is locked | |
| 835 | + * @param {PersonaLockType} type - Lock type | |
| 612 | 836 | * @returns {boolean} Whether the persona is locked |
| 613 | 837 | */ |
| 614 | 838 | export function isPersonaLocked(type = 'chat') { |
| 615 | - return !!chat_metadata['persona']; | |
| 839 | + switch (type) { | |
| 840 | + case 'default': | |
| 841 | + return power_user.default_persona === user_avatar; | |
| 842 | + case 'chat': | |
| 843 | + return chat_metadata['persona'] == user_avatar; | |
| 844 | + case 'character': { | |
| 845 | + return !!power_user.persona_descriptions[user_avatar]?.connections?.some(isPersonaConnectionLocked); | |
| 846 | + } | |
| 847 | + default: throw new Error(`Unknown persona lock type: ${type}`); | |
| 848 | + } | |
| 616 | 849 | } |
| 617 | 850 | |
| 618 | 851 | /** |
| 619 | 852 | * Locks or unlocks the persona for the current chat. |
| 620 | 853 | * @param {boolean} state Desired lock state |
| 854 | + * @param {PersonaLockType} type - Lock type | |
| 621 | 855 | * @returns {Promise<void>} |
| 622 | 856 | */ |
| 623 | 857 | export async function setPersonaLockState(state, type = 'chat') { |
| 624 | 858 | return state ? await lockPersona(type) : await unlockPersona(type); |
| 625 | 859 | } |
| 626 | 860 | |
| 627 | 861 | /** |
| 628 | 862 | * Toggle the persona lock state for the current chat. |
| 629 | - * @returns {Promise<void>} | |
| 863 | + * @param {PersonaLockType} type - Lock type | |
| 864 | + * @returns {Promise<boolean>} - Whether the persona was locked | |
| 630 | 865 | */ |
| 631 | 866 | export async function togglePersonaLock(type = 'chat') { |
| 632 | 867 | returnif (isPersonaLocked(type)) { |
| 633 | 868 | ? await unlockPersona(type); |
| 634 | - : await lockPersona(); | |
| 869 | + return false; | |
| 870 | + } else { | |
| 871 | + await lockPersona(type); | |
| 872 | + return true; | |
| 873 | + } | |
| 635 | 874 | } |
| 636 | 875 | |
| 637 | 876 | /** |
| 638 | 877 | * Unlock the persona for the current chat. |
| 878 | + * @param {PersonaLockType} type - Lock type | |
| 879 | + * @returns {Promise<void>} | |
| 639 | 880 | */ |
| 640 | 881 | async function unlockPersona(type = 'chat') { |
| 641 | 882 | ifswitch (chat_metadata['persona']type) { |
| 642 | - console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); | |
| 883 | + case 'default': { | |
| 643 | - delete chat_metadata['persona']; | |
| 884 | + // TODO: Make this toggle-able | |
| 644 | - await saveMetadata(); | |
| 885 | + await toggleDefaultPersona(user_avatar, { quiet: true }); | |
| 645 | - if (power_user.persona_show_notifications) { | |
| 886 | + break; | |
| 646 | - toastr.info(t`User persona is now unlocked for this chat. Click the "Lock" again to revert.`, t`Persona unlocked`); | |
| 887 | + } | |
| 888 | + case 'chat': { | |
| 889 | + if (chat_metadata['persona']) { | |
| 890 | + console.log(`Unlocking persona ${user_avatar} from this chat`); | |
| 891 | + delete chat_metadata['persona']; | |
| 892 | + await saveMetadata(); | |
| 893 | + if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { | |
| 894 | + toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`); | |
| 895 | + } | |
| 896 | + } | |
| 897 | + break; | |
| 647 | 898 | } |
| 648 | - updateUserLockIcon(); | |
| 899 | + case 'character': { | |
| 900 | + /** @type {PersonaConnection[]} */ | |
| 901 | + const connections = power_user.persona_descriptions[user_avatar]?.connections; | |
| 902 | + if (connections) { | |
| 903 | + console.log(`Unlocking persona ${user_avatar} from this character ${name2}`); | |
| 904 | + power_user.persona_descriptions[user_avatar].connections = connections.filter(c => !isPersonaConnectionLocked(c)); | |
| 905 | + saveSettingsDebounced(); | |
| 906 | + updatePersonaConnectionsAvatarList(); | |
| 907 | + if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { | |
| 908 | + toastr.info(t`Persona ${name1} is now unlocked from character ${name2}.`, t`Persona Unlocked`); | |
| 909 | + } | |
| 910 | + } | |
| 911 | + break; | |
| 912 | + } | |
| 913 | + default: | |
| 914 | + throw new Error(`Unknown persona lock type: ${type}`); | |
| 649 | 915 | } |
| 916 | + | |
| 917 | + updatePersonaUIStates(); | |
| 650 | 918 | } |
| 651 | 919 | |
| 652 | 920 | /** |
| 653 | 921 | * Lock the persona for the current chat. |
| 922 | + * @param {PersonaLockType} type - Lock type | |
| 654 | 923 | */ |
| 655 | 924 | async function lockPersona(type = 'chat') { |
| 925 | + // First make sure that user_avatar is actually a persona | |
| 656 | 926 | if (!(user_avatar in power_user.personas)) { |
| 657 | 927 | console.log(`Creating a new persona ${user_avatar}`); |
| 658 | 928 | if (power_user.persona_show_notifications) { |
| 659 | - toastr.info( | |
| 929 | + toastr.info(t`Creating a new persona for currently selected user name and avatar...`, t`Persona Not Found`); | |
| 660 | - t`Creating a new persona for currently selected user name and avatar...`, | |
| 661 | - t`Persona not set for this avatar`, | |
| 662 | - { timeOut: 10000, extendedTimeOut: 20000 }, | |
| 663 | - ); | |
| 664 | 930 | } |
| 665 | 931 | power_user.personas[user_avatar] = name1; |
| 666 | 932 | power_user.persona_descriptions[user_avatar] = { |
| @@ -669,36 +935,75 @@ async function lockPersona() { | ||
| 669 | 935 | depth: DEFAULT_DEPTH, |
| 670 | 936 | role: DEFAULT_ROLE, |
| 671 | 937 | lorebook: '', |
| 938 | + connections: [], | |
| 672 | 939 | }; |
| 673 | 940 | } |
| 674 | 941 | |
| 675 | - chat_metadata['persona'] = user_avatar; | |
| 942 | + switch (type) { | |
| 676 | - await saveMetadata(); | |
| 943 | + case 'default': { | |
| 677 | - saveSettingsDebounced(); | |
| 944 | + await toggleDefaultPersona(user_avatar, { quiet: true }); | |
| 678 | - console.log(`Locking persona for this chat ${user_avatar}`); | |
| 945 | + break; | |
| 679 | - if (power_user.persona_show_notifications) { | |
| 946 | + } | |
| 680 | - toastr.success(t`User persona is locked to ${name1} in this chat`); | |
| 947 | + case 'chat': { | |
| 948 | + console.log(`Locking persona ${user_avatar} to this chat`); | |
| 949 | + chat_metadata['persona'] = user_avatar; | |
| 950 | + saveMetadataDebounced(); | |
| 951 | + if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { | |
| 952 | + toastr.success(t`User persona ${name1} is locked to ${name2} in this chat`, t`Persona Locked`); | |
| 953 | + } | |
| 954 | + break; | |
| 955 | + } | |
| 956 | + case 'character': { | |
| 957 | + const newConnection = getCurrentConnectionObj(); | |
| 958 | + /** @type {PersonaConnection[]} */ | |
| 959 | + const connections = power_user.persona_descriptions[user_avatar].connections?.filter(c => !isPersonaConnectionLocked(c)) ?? []; | |
| 960 | + if (newConnection && newConnection.id) { | |
| 961 | + console.log(`Locking persona ${user_avatar} to this character ${name2}`); | |
| 962 | + power_user.persona_descriptions[user_avatar].connections = [...connections, newConnection]; | |
| 963 | + | |
| 964 | + const unlinkedCharacters = []; | |
| 965 | + if (!power_user.persona_allow_multi_connections) { | |
| 966 | + for (const [avatarId, description] of Object.entries(power_user.persona_descriptions)) { | |
| 967 | + if (avatarId === user_avatar) continue; | |
| 968 | + | |
| 969 | + const filteredConnections = description.connections?.filter(c => !(c.type === newConnection.type && c.id === newConnection.id)) ?? []; | |
| 970 | + if (filteredConnections.length !== description.connections?.length) { | |
| 971 | + description.connections = filteredConnections; | |
| 972 | + unlinkedCharacters.push(power_user.personas[avatarId]); | |
| 973 | + } | |
| 974 | + } | |
| 975 | + } | |
| 976 | + | |
| 977 | + saveSettingsDebounced(); | |
| 978 | + updatePersonaConnectionsAvatarList(); | |
| 979 | + if (power_user.persona_show_notifications) { | |
| 980 | + let additional = ''; | |
| 981 | + if (unlinkedCharacters.length) | |
| 982 | + additional += `<br /><br />${t`Unlinked existing persona${unlinkedCharacters.length > 1 ? 's' : ''}: ${unlinkedCharacters.join(', ')}`}`; | |
| 983 | + if (additional || !isPersonaPanelOpen()) { | |
| 984 | + toastr.success(t`User persona ${name1} is locked to character ${name2}${additional}`, t`Persona Locked`, { escapeHtml: false }); | |
| 985 | + } | |
| 986 | + } | |
| 987 | + } | |
| 988 | + break; | |
| 989 | + } | |
| 990 | + default: | |
| 991 | + throw new Error(`Unknown persona lock type: ${type}`); | |
| 681 | 992 | } |
| 682 | - updateUserLockIcon(); | |
| 993 | + | |
| 994 | + updatePersonaUIStates(); | |
| 683 | 995 | } |
| 684 | 996 | |
| 685 | 997 | |
| 686 | 998 | async function deleteUserAvatar(e) { |
| 687 | - e?.stopPropagation(); | |
| 999 | + const avatarId = user_avatar; | |
| 688 | - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | |
| 689 | 1000 | |
| 690 | 1001 | if (!avatarId) { |
| 691 | 1002 | console.warn('No avatar id found'); |
| 692 | 1003 | return; |
| 693 | 1004 | } |
| 694 | - | |
| 1005 | + const confirm = await Popup.show.confirm(t`Delete Persona`, | |
| 695 | - if (avatarId == user_avatar) { | |
| 1006 | + t`Are you sure you want to delete this avatar?` + '<br />' + t`All information associated with its linked persona will be lost.`); | |
| 696 | - console.warn(`User tried to delete their current avatar ${avatarId}`); | |
| 697 | - toastr.warning(t`You cannot delete the avatar you are currently using`, t`Warning`); | |
| 698 | - return; | |
| 699 | - } | |
| 700 | - | |
| 701 | - const confirm = await Popup.show.confirm(t`Are you sure you want to delete this avatar?`, t`All information associated with its linked persona will be lost.`); | |
| 702 | 1007 | |
| 703 | 1008 | if (!confirm) { |
| 704 | 1009 | console.debug('User cancelled deleting avatar'); |
| @@ -719,19 +1024,20 @@ async function deleteUserAvatar(e) { | ||
| 719 | 1024 | delete power_user.persona_descriptions[avatarId]; |
| 720 | 1025 | |
| 721 | 1026 | if (avatarId === power_user.default_persona) { |
| 722 | 1027 | toastr.warning(t`The default persona was deleted. You will need to set a new default persona.`, t`Default personaPersona deletedDeleted`); |
| 723 | 1028 | power_user.default_persona = null; |
| 724 | 1029 | } |
| 725 | 1030 | |
| 726 | 1031 | if (avatarId === chat_metadata['persona']) { |
| 727 | 1032 | toastr.warning(t`The locked persona was deleted. You will need to set a new persona for this chat.`, t`Persona deletedDeleted`); |
| 728 | 1033 | delete chat_metadata['persona']; |
| 729 | 1034 | await saveMetadata(); |
| 730 | 1035 | } |
| 731 | 1036 | |
| 732 | 1037 | saveSettingsDebounced(); |
| 733 | - await getUserAvatars(); | |
| 1038 | + | |
| 734 | - updateUserLockIcon(); | |
| 1039 | + // Use the existing mechanism to re-render the persona list and choose the next persona here | |
| 1040 | + await loadPersonaForCurrentChat({ doRender: true }); | |
| 735 | 1041 | } |
| 736 | 1042 | } |
| 737 | 1043 | |
| @@ -786,14 +1092,14 @@ function onPersonaDescriptionDepthRoleInput() { | ||
| 786 | 1092 | |
| 787 | 1093 | /** |
| 788 | 1094 | * Opens a popup to set the lorebook for the current persona. |
| 789 | 1095 | * @param {PointerEventJQuery.ClickEvent} event Click event |
| 790 | 1096 | */ |
| 791 | 1097 | async function onPersonaLoreButtonClick(event) { |
| 792 | 1098 | const personaName = power_user.personas[user_avatar]; |
| 793 | 1099 | const selectedLorebook = power_user.persona_description_lorebook; |
| 794 | 1100 | |
| 795 | 1101 | if (!personaName) { |
| 796 | 1102 | toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona nameName notNot setSet`); |
| 797 | 1103 | return; |
| 798 | 1104 | } |
| 799 | 1105 | |
| @@ -854,16 +1160,21 @@ function getOrCreatePersonaDescriptor() { | ||
| 854 | 1160 | depth: power_user.persona_description_depth, |
| 855 | 1161 | role: power_user.persona_description_role, |
| 856 | 1162 | lorebook: power_user.persona_description_lorebook, |
| 1163 | + connections: [], | |
| 857 | 1164 | }; |
| 858 | 1165 | power_user.persona_descriptions[user_avatar] = object; |
| 859 | 1166 | } |
| 860 | 1167 | return object; |
| 861 | 1168 | } |
| 862 | 1169 | |
| 863 | -async function setDefaultPersona(e) { | |
| 1170 | +/** | |
| 864 | - e?.stopPropagation(); | |
| 1171 | + * Sets a persona as the default one to be used for all new chats and unlocked existing chats | |
| 865 | - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | |
| 1172 | + * @param {string} avatarId The avatar id of the persona to set as the default | |
| 866 | - | |
| 1173 | + * @param {object} [options] Optional arguments | |
| 1174 | + * @param {boolean} [options.quiet=false] If true, no confirmation popups will be shown | |
| 1175 | + * @returns {Promise<void>} | |
| 1176 | + */ | |
| 1177 | +async function toggleDefaultPersona(avatarId, { quiet = false } = {}) { | |
| 867 | 1178 | if (!avatarId) { |
| 868 | 1179 | console.warn('No avatar id found'); |
| 869 | 1180 | return; |
| @@ -873,78 +1184,264 @@ async function setDefaultPersona(e) { | ||
| 873 | 1184 | |
| 874 | 1185 | if (power_user.personas[avatarId] === undefined) { |
| 875 | 1186 | console.warn(`No persona name found for avatar ${avatarId}`); |
| 876 | 1187 | toastr.warning(t`You must bind a name to this persona before you can set it as the default.`, t`Persona nameName notNot setSet`); |
| 877 | 1188 | return; |
| 878 | 1189 | } |
| 879 | 1190 | |
| 880 | - const personaName = power_user.personas[avatarId]; | |
| 881 | 1191 | |
| 882 | 1192 | if (avatarId === currentDefault) { |
| 883 | - const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, personaName); | |
| 1193 | + if (!quiet) { | |
| 884 | - | |
| 1194 | + const confirm = await Popup.show.confirm(t`Are you sure you want to remove the default persona?`, power_user.personas[avatarId]); | |
| 885 | 1195 | if (!confirm) { |
| 886 | 1196 | console.debug('User cancelled removing default persona'); |
| 887 | 1197 | return; |
| 1198 | + } | |
| 888 | 1199 | } |
| 889 | 1200 | |
| 890 | 1201 | console.log(`Removing default persona ${avatarId}`); |
| 891 | 1202 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 892 | 1203 | toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default personaPersona removedRemoved`); |
| 893 | 1204 | } |
| 894 | 1205 | delete power_user.default_persona; |
| 895 | 1206 | } else { |
| 896 | - const confirm = await Popup.show.confirm(t`Are you sure you want to set \"${personaName}\" as the default persona?`, t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`); | |
| 1207 | + if (!quiet) { | |
| 897 | - | |
| 1208 | + const confirm = await Popup.show.confirm(t`Set Default Persona`, | |
| 898 | - if (!confirm) { | |
| 1209 | + t`Are you sure you want to set \"${power_user.personas[avatarId]}\" as the default persona?` | |
| 899 | - console.debug('User cancelled setting default persona'); | |
| 1210 | + + '<br /><br />' | |
| 900 | - return; | |
| 1211 | + + t`This name and avatar will be used for all new chats, as well as existing chats where the user persona is not locked.`); | |
| 1212 | + if (!confirm) { | |
| 1213 | + console.debug('User cancelled setting default persona'); | |
| 1214 | + return; | |
| 1215 | + } | |
| 901 | 1216 | } |
| 902 | 1217 | |
| 903 | 1218 | power_user.default_persona = avatarId; |
| 904 | 1219 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 905 | 1220 | toastr.success(t`Set to ${power_user.personas[avatarId]}.This persona will be used by default when you open a new chat.`, t`Default persona set to ${personaName}Persona`); |
| 906 | 1221 | } |
| 907 | 1222 | } |
| 908 | 1223 | |
| 909 | 1224 | saveSettingsDebounced(); |
| 910 | 1225 | await getUserAvatars(true, avatarId); |
| 1226 | + updatePersonaUIStates(); | |
| 1227 | +} | |
| 1228 | + | |
| 1229 | +/** | |
| 1230 | + * Returns an object with 3 properties that describe the state of the given persona | |
| 1231 | + * | |
| 1232 | + * - default: Whether this persona is the default one for all new chats | |
| 1233 | + * - locked: An object containing the lock states | |
| 1234 | + * - chat: Whether the persona is locked to the currently open chat | |
| 1235 | + * - character: Whether the persona is locked to the currently open character or group | |
| 1236 | + * @param {string} avatarId - The avatar id of the persona to get the state for | |
| 1237 | + * @returns {PersonaState} An object describing the state of the given persona | |
| 1238 | + */ | |
| 1239 | +function getPersonaStates(avatarId) { | |
| 1240 | + const isDefaultPersona = power_user.default_persona === avatarId; | |
| 1241 | + const hasChatLock = chat_metadata['persona'] == avatarId; | |
| 1242 | + | |
| 1243 | + /** @type {PersonaConnection[]} */ | |
| 1244 | + const connections = power_user.persona_descriptions[avatarId]?.connections; | |
| 1245 | + const hasCharLock = !!connections?.some(c => | |
| 1246 | + (!selected_group && c.type === 'character' && c.id === characters[this_chid]?.avatar) | |
| 1247 | + || (selected_group && c.type === 'group' && c.id === selected_group)); | |
| 1248 | + | |
| 1249 | + return { | |
| 1250 | + avatarId: avatarId, | |
| 1251 | + default: isDefaultPersona, | |
| 1252 | + locked: { | |
| 1253 | + chat: hasChatLock, | |
| 1254 | + character: hasCharLock, | |
| 1255 | + }, | |
| 1256 | + }; | |
| 1257 | +} | |
| 1258 | + | |
| 1259 | +/** | |
| 1260 | + * Updates the UI to reflect the current states of all personas and the selected user's persona. | |
| 1261 | + * This includes updating class states on avatar containers to indicate default status, chat lock, | |
| 1262 | + * and character lock, as well as updating icons and labels in the persona management panel to reflect | |
| 1263 | + * the current state of the user's persona. | |
| 1264 | + * Additionally, it manages the display of temporary persona lock information. | |
| 1265 | + * @param {Object} [options={}] - Optional settings | |
| 1266 | + * @param {boolean} [options.navigateToCurrent=false] - Whether to navigate to the current persona in the persona list | |
| 1267 | + */ | |
| 1268 | + | |
| 1269 | +function updatePersonaUIStates({ navigateToCurrent = false } = {}) { | |
| 1270 | + if (navigateToCurrent) { | |
| 1271 | + navigateToAvatar(user_avatar); | |
| 1272 | + } | |
| 1273 | + | |
| 1274 | + // Update the persona list | |
| 1275 | + $('#user_avatar_block .avatar-container').each(function () { | |
| 1276 | + const avatarId = $(this).attr('data-avatar-id'); | |
| 1277 | + const states = getPersonaStates(avatarId); | |
| 1278 | + $(this).toggleClass('default_persona', states.default); | |
| 1279 | + $(this).toggleClass('locked_to_chat', states.locked.chat); | |
| 1280 | + $(this).toggleClass('locked_to_character', states.locked.character); | |
| 1281 | + $(this).toggleClass('selected', avatarId === user_avatar); | |
| 1282 | + }); | |
| 1283 | + | |
| 1284 | + // Buttons for the persona panel on the right | |
| 1285 | + const personaStates = getPersonaStates(user_avatar); | |
| 1286 | + | |
| 1287 | + $('#lock_persona_default').toggleClass('locked', personaStates.default); | |
| 1288 | + | |
| 1289 | + $('#lock_user_name').toggleClass('locked', personaStates.locked.chat); | |
| 1290 | + $('#lock_user_name i.icon').toggleClass('fa-lock', personaStates.locked.chat); | |
| 1291 | + $('#lock_user_name i.icon').toggleClass('fa-unlock', !personaStates.locked.chat); | |
| 1292 | + | |
| 1293 | + $('#lock_persona_to_char').toggleClass('locked', personaStates.locked.character); | |
| 1294 | + $('#lock_persona_to_char i.icon').toggleClass('fa-lock', personaStates.locked.character); | |
| 1295 | + $('#lock_persona_to_char i.icon').toggleClass('fa-unlock', !personaStates.locked.character); | |
| 1296 | + | |
| 1297 | + // Persona panel info block | |
| 1298 | + const { isTemporary, info } = getPersonaTemporaryLockInfo(); | |
| 1299 | + if (isTemporary) { | |
| 1300 | + const messageContainer = document.createElement('div'); | |
| 1301 | + const messageSpan = document.createElement('span'); | |
| 1302 | + messageSpan.textContent = t`Temporary persona in use.`; | |
| 1303 | + messageContainer.appendChild(messageSpan); | |
| 1304 | + messageContainer.classList.add('flex-container', 'alignItemsBaseline'); | |
| 1305 | + | |
| 1306 | + const infoIcon = document.createElement('i'); | |
| 1307 | + infoIcon.classList.add('fa-solid', 'fa-circle-info', 'opacity50p'); | |
| 1308 | + infoIcon.title = info; | |
| 1309 | + messageContainer.appendChild(infoIcon); | |
| 1310 | + | |
| 1311 | + // Set the info block content | |
| 1312 | + setInfoBlock('#persona_connections_info_block', messageContainer, 'hint'); | |
| 1313 | + } else { | |
| 1314 | + // Clear the info block if no condition applies | |
| 1315 | + clearInfoBlock('#persona_connections_info_block'); | |
| 1316 | + } | |
| 911 | 1317 | } |
| 912 | 1318 | |
| 913 | -function updateUserLockIcon() { | |
| 1319 | +/** | |
| 914 | - const hasLock = !!chat_metadata['persona']; | |
| 1320 | + * @typedef {Object} PersonaLockInfo | |
| 915 | - $('#lock_user_name').toggleClass('fa-unlock', !hasLock); | |
| 1321 | + * @property {boolean} isTemporary - Whether the selected persona is temporary based on current locks. | |
| 916 | - $('#lock_user_name').toggleClass('fa-lock', hasLock); | |
| 1322 | + * @property {boolean} hasDifferentChatLock - True if the chat persona is set and differs from the user avatar. | |
| 1323 | + * @property {boolean} hasDifferentDefaultLock - True if the default persona is set and differs from the user avatar. | |
| 1324 | + * @property {string} info - Detailed information about the current, chat, and default personas. | |
| 1325 | + */ | |
| 1326 | + | |
| 1327 | +/** | |
| 1328 | + * Computes temporary lock information for the current persona. | |
| 1329 | + * | |
| 1330 | + * This function checks whether the currently selected persona is temporary by comparing | |
| 1331 | + * the chat persona and the default persona to the user avatar. If either is different, | |
| 1332 | + * the currently selected persona is considered temporary and a detailed message is generated. | |
| 1333 | + * | |
| 1334 | + * @returns {PersonaLockInfo} An object containing flags and a message describing the persona lock status. | |
| 1335 | + */ | |
| 1336 | +function getPersonaTemporaryLockInfo() { | |
| 1337 | + const hasDifferentChatLock = !!chat_metadata['persona'] && chat_metadata['persona'] !== user_avatar; | |
| 1338 | + const hasDifferentDefaultLock = power_user.default_persona && power_user.default_persona !== user_avatar; | |
| 1339 | + const isTemporary = hasDifferentChatLock || (!chat_metadata['persona'] && hasDifferentDefaultLock); | |
| 1340 | + const info = isTemporary ? t`A different persona is locked to this chat, or you have a different default persona set. The currently selected persona will only be temporary, and resets on reload. Consider locking this persona to the chat if you want to permanently use it.` | |
| 1341 | + + '\n\n' | |
| 1342 | + + t`Current Persona: ${power_user.personas[user_avatar]}` | |
| 1343 | + + (hasDifferentChatLock ? '\n' + t`Chat persona: ${power_user.personas[chat_metadata['persona']]}` : '') | |
| 1344 | + + (hasDifferentDefaultLock ? '\n' + t`Default persona: ${power_user.personas[power_user.default_persona]}` : '') : ''; | |
| 1345 | + | |
| 1346 | + return { | |
| 1347 | + isTemporary: isTemporary, | |
| 1348 | + hasDifferentChatLock: hasDifferentChatLock, | |
| 1349 | + hasDifferentDefaultLock: hasDifferentDefaultLock, | |
| 1350 | + info: info, | |
| 1351 | + }; | |
| 917 | 1352 | } |
| 918 | 1353 | |
| 919 | -async function setChatLockedPersona() { | |
| 1354 | +/** | |
| 1355 | + * Loads the appropriate persona for the current chat session based on locks (chat lock, char lock, default persona) | |
| 1356 | + * | |
| 1357 | + * @param {Object} [options={}] - Optional arguments | |
| 1358 | + * @param {boolean} [options.doRender=false] - Whether to render the persona immediately | |
| 1359 | + * @returns {Promise<boolean>} - A promise that resolves to a boolean indicating whether a persona was selected | |
| 1360 | + */ | |
| 1361 | +async function loadPersonaForCurrentChat({ doRender = false } = {}) { | |
| 1362 | + // Cache persona list to check if they exist | |
| 1363 | + const userAvatars = await getUserAvatars(doRender); | |
| 1364 | + | |
| 920 | 1365 | // Define a persona for this chat |
| 921 | 1366 | let chatPersona = ''; |
| 922 | 1367 | |
| 1368 | + /** @type {'chat' | 'character' | 'default' | null} */ | |
| 1369 | + let connectType = null; | |
| 1370 | + | |
| 1371 | + // If persona is locked in chat metadata, select it | |
| 923 | 1372 | if (chat_metadata['persona']) { |
| 924 | - // If persona is locked in chat metadata, select it | |
| 925 | 1373 | console.log(`Using locked persona ${chat_metadata['persona']}`); |
| 926 | 1374 | chatPersona = chat_metadata['persona']; |
| 927 | - } else if (power_user.default_persona) { | |
| 1375 | + | |
| 928 | - // If default persona is set, select it | |
| 1376 | + // Verify it exists | |
| 929 | - console.log(`Using default persona ${power_user.default_persona}`); | |
| 1377 | + if (!userAvatars.includes(chatPersona)) { | |
| 930 | - chatPersona = power_user.default_persona; | |
| 1378 | + console.warn('Chat-locked persona avatar not found, unlocking persona'); | |
| 1379 | + delete chat_metadata['persona']; | |
| 1380 | + saveSettingsDebounced(); | |
| 1381 | + chatPersona = ''; | |
| 1382 | + } | |
| 1383 | + if (chatPersona) connectType = 'chat'; | |
| 931 | 1384 | } |
| 932 | 1385 | |
| 933 | - // No persona set: user current settings | |
| 1386 | + // If the persona panel is open when the chat changes, this is likely because a character was selected from that panel. | |
| 1387 | + // In that case, we are not automatically switching persona - but need to make changes if there is any chat-bound connection | |
| 1388 | + /* | |
| 1389 | + if (isPersonaPanelOpen()) { | |
| 1390 | + if (chatPersona) { | |
| 1391 | + // If the chat-bound persona is the currently selected one, we can simply exit out | |
| 1392 | + if (chatPersona === user_avatar) { | |
| 1393 | + return false; | |
| 1394 | + } | |
| 1395 | + // Otherwise ask if we want to switch | |
| 1396 | + const autoLock = power_user.persona_auto_lock; | |
| 1397 | + const result = await Popup.show.confirm(t`Switch Persona?`, | |
| 1398 | + t`You have a connected persona for the current chat (${power_user.personas[chatPersona]}). Do you want to stick to the current persona (${power_user.personas[user_avatar]}) ${(autoLock ? t`and lock that to the chat` : '')}, or switch to ${power_user.personas[chatPersona]} instead?`, | |
| 1399 | + { okButton: autoLock ? t`Keep and Lock` : t`Keep`, cancelButton: t`Switch` }); | |
| 1400 | + if (result === POPUP_RESULT.AFFIRMATIVE) { | |
| 1401 | + if (autoLock) { | |
| 1402 | + lockPersona('chat'); | |
| 1403 | + } | |
| 1404 | + return false; | |
| 1405 | + } | |
| 1406 | + } else { | |
| 1407 | + // If we don't have a chat-bound persona, we simply return and keep the current one we have | |
| 1408 | + return false; | |
| 1409 | + } | |
| 1410 | + } | |
| 1411 | + */ | |
| 1412 | + | |
| 1413 | + // Check if we have any persona connected to the current character | |
| 934 | 1414 | if (!chatPersona) { |
| 935 | - console.debug('No default or locked persona set for this chat'); | |
| 1415 | + const connectedPersonas = getConnectedPersonas(); | |
| 936 | - return; | |
| 1416 | + | |
| 1417 | + if (connectedPersonas.length > 0) { | |
| 1418 | + if (connectedPersonas.length === 1) { | |
| 1419 | + chatPersona = connectedPersonas[0]; | |
| 1420 | + } else if (!power_user.persona_allow_multi_connections) { | |
| 1421 | + console.warn('More than one persona is connected to this character.Using the first available persona for this chat.'); | |
| 1422 | + chatPersona = connectedPersonas[0]; | |
| 1423 | + } else { | |
| 1424 | + chatPersona = await askForPersonaSelection(t`Select Persona`, | |
| 1425 | + t`Multiple personas are connected to this character.\nSelect a persona to use for this chat.`, | |
| 1426 | + connectedPersonas, { highlightPersonas: true, targetedChar: getCurrentConnectionObj() }); | |
| 1427 | + } | |
| 1428 | + } | |
| 1429 | + | |
| 1430 | + if (chatPersona) connectType = 'character'; | |
| 937 | 1431 | } |
| 938 | 1432 | |
| 939 | - // Find the avatar file | |
| 1433 | + // Last check if default persona is set, select it | |
| 940 | - const userAvatars = await getUserAvatars(false); | |
| 1434 | + if (!chatPersona && power_user.default_persona) { | |
| 1435 | + console.log(`Using default persona ${power_user.default_persona}`); | |
| 1436 | + chatPersona = power_user.default_persona; | |
| 1437 | + | |
| 1438 | + if (chatPersona) connectType = 'default'; | |
| 1439 | + } | |
| 941 | 1440 | |
| 942 | - // Avatar missing (persona deleted) | |
| 1441 | + // Whatever way we selected a persona, if it doesn't exist, unlock this chat | |
| 943 | 1442 | if (chat_metadata['persona'] && !userAvatars.includes(chatPersonachat_metadata['persona'])) { |
| 944 | 1443 | console.warn('Persona avatar not found, unlocking persona'); |
| 945 | 1444 | delete chat_metadata['persona']; |
| 946 | - updateUserLockIcon(); | |
| 947 | - return; | |
| 948 | 1445 | } |
| 949 | 1446 | |
| 950 | 1447 | // Default persona missing |
| @@ -952,12 +1449,101 @@ async function setChatLockedPersona() { | ||
| 952 | 1449 | console.warn('Default persona avatar not found, clearing default persona'); |
| 953 | 1450 | power_user.default_persona = null; |
| 954 | 1451 | saveSettingsDebounced(); |
| 955 | - return; | |
| 956 | 1452 | } |
| 957 | 1453 | |
| 958 | 1454 | // Persona avatar found, select it |
| 959 | - setUserAvatar(chatPersona); | |
| 1455 | + if (chatPersona && user_avatar !== chatPersona) { | |
| 960 | - updateUserLockIcon(); | |
| 1456 | + const willAutoLock = power_user.persona_auto_lock && user_avatar !== chat_metadata['persona']; | |
| 1457 | + setUserAvatar(chatPersona, { toastPersonaNameChange: false, navigateToCurrent: true }); | |
| 1458 | + | |
| 1459 | + if (power_user.persona_show_notifications) { | |
| 1460 | + let message = t`Auto-selected persona based on ${connectType} connection.<br />Your messages will now be sent as ${power_user.personas[chatPersona]}.`; | |
| 1461 | + if (willAutoLock) { | |
| 1462 | + message += '<br /><br />' + t`Auto-locked this persona to current chat.`; | |
| 1463 | + } | |
| 1464 | + toastr.success(message, t`Persona Auto Selected`, { escapeHtml: false }); | |
| 1465 | + } | |
| 1466 | + } | |
| 1467 | + | |
| 1468 | + updatePersonaUIStates(); | |
| 1469 | + | |
| 1470 | + return !!chatPersona; | |
| 1471 | +} | |
| 1472 | + | |
| 1473 | +/** | |
| 1474 | + * Returns an array of persona keys that are connected to the given character key. | |
| 1475 | + * If the character key is not provided, it defaults to the currently selected group or character. | |
| 1476 | + * @param {string} [characterKey] - The character key to query | |
| 1477 | + * @returns {string[]} - An array of persona keys that are connected to the given character key | |
| 1478 | + */ | |
| 1479 | +export function getConnectedPersonas(characterKey = undefined) { | |
| 1480 | + characterKey ??= selected_group || characters[this_chid]?.avatar; | |
| 1481 | + const connectedPersonas = Object.entries(power_user.persona_descriptions) | |
| 1482 | + .filter(([_, desc]) => desc.connections?.some(conn => conn.type === 'character' && conn.id === characterKey)) | |
| 1483 | + .map(([key, _]) => key); | |
| 1484 | + return connectedPersonas; | |
| 1485 | +} | |
| 1486 | + | |
| 1487 | + | |
| 1488 | +/** | |
| 1489 | + * Shows a popup with all personas connected to the currently selected character or group. | |
| 1490 | + * In the popup, the user can select a persona to load for the current character or group, or shift-click to remove the connection. | |
| 1491 | + * @return {Promise<void>} | |
| 1492 | + */ | |
| 1493 | +export async function showCharConnections() { | |
| 1494 | + let isRemoving = false; | |
| 1495 | + | |
| 1496 | + const connections = getConnectedPersonas(); | |
| 1497 | + const message = t`The following personas are connected to the current character.\n\nClick on a persona to select it for the current character.\nShift + Click to unlink the persona from the character.`; | |
| 1498 | + const selectedPersona = await askForPersonaSelection(t`Persona Connections`, message, connections, { | |
| 1499 | + okButton: t`Ok`, | |
| 1500 | + highlightPersonas: true, | |
| 1501 | + targetedChar: getCurrentConnectionObj(), | |
| 1502 | + shiftClickHandler: (element, ev) => { | |
| 1503 | + | |
| 1504 | + const personaId = $(element).attr('data-pid'); | |
| 1505 | + | |
| 1506 | + /** @type {PersonaConnection[]} */ | |
| 1507 | + const connections = power_user.persona_descriptions[personaId]?.connections; | |
| 1508 | + if (connections) { | |
| 1509 | + console.log(`Unlocking persona ${personaId} from current character ${name2}`); | |
| 1510 | + power_user.persona_descriptions[personaId].connections = connections.filter(c => { | |
| 1511 | + if (menu_type == 'group_edit' && c.type == 'group' && c.id == selected_group) return false; | |
| 1512 | + else if (c.type == 'character' && c.id == characters[this_chid]?.avatar) return false; | |
| 1513 | + return true; | |
| 1514 | + }); | |
| 1515 | + saveSettingsDebounced(); | |
| 1516 | + updatePersonaConnectionsAvatarList(); | |
| 1517 | + if (power_user.persona_show_notifications) { | |
| 1518 | + toastr.info(t`User persona ${power_user.personas[personaId]} is now unlocked from the current character ${name2}.`, t`Persona unlocked`); | |
| 1519 | + } | |
| 1520 | + | |
| 1521 | + isRemoving = true; | |
| 1522 | + $('#char_connections_button').trigger('click'); | |
| 1523 | + } | |
| 1524 | + }, | |
| 1525 | + }); | |
| 1526 | + | |
| 1527 | + // One of the persona was selected. So load it. | |
| 1528 | + if (!isRemoving && selectedPersona) { | |
| 1529 | + setUserAvatar(selectedPersona, { toastPersonaNameChange: false }); | |
| 1530 | + if (power_user.persona_show_notifications) { | |
| 1531 | + toastr.success(t`Selected persona ${power_user.personas[selectedPersona]} for current chat.`, t`Connected Persona Selected`); | |
| 1532 | + } | |
| 1533 | + } | |
| 1534 | +} | |
| 1535 | + | |
| 1536 | +/** | |
| 1537 | + * Retrieves the current connection object based on whether the current chat is with a char or a group. | |
| 1538 | + * | |
| 1539 | + * @returns {PersonaConnection} An object representing the current connection | |
| 1540 | + */ | |
| 1541 | +export function getCurrentConnectionObj() { | |
| 1542 | + if (selected_group) | |
| 1543 | + return { type: 'group', id: selected_group }; | |
| 1544 | + if (characters[this_chid]?.avatar) | |
| 1545 | + return { type: 'character', id: characters[this_chid]?.avatar }; | |
| 1546 | + return null; | |
| 961 | 1547 | } |
| 962 | 1548 | |
| 963 | 1549 | function onBackupPersonas() { |
| @@ -1038,10 +1624,10 @@ async function onPersonasRestoreInput(e) { | ||
| 1038 | 1624 | } |
| 1039 | 1625 | |
| 1040 | 1626 | if (warnings.length) { |
| 1041 | 1627 | toastr.success(t`Personas restored with warnings. Check console for details.`, t`Persona Management`); |
| 1042 | 1628 | console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`); |
| 1043 | 1629 | } else { |
| 1044 | 1630 | toastr.success(t`Personas restored successfully.`, t`Persona Management`); |
| 1045 | 1631 | } |
| 1046 | 1632 | |
| 1047 | 1633 | await getUserAvatars(); |
| @@ -1068,6 +1654,11 @@ async function syncUserNameToPersona() { | ||
| 1068 | 1654 | await reloadCurrentChat(); |
| 1069 | 1655 | } |
| 1070 | 1656 | |
| 1657 | +/** | |
| 1658 | + * Retriggers the first message to reload it from the char definition. | |
| 1659 | + * | |
| 1660 | + * Only works if only the first message is present, and not in group mode. | |
| 1661 | + */ | |
| 1071 | 1662 | export function retriggerFirstMessageOnEmptyChat() { |
| 1072 | 1663 | if (this_chid >= 0 && !selected_group && chat.length === 1) { |
| 1073 | 1664 | $('#firstmessage_textarea').trigger('input'); |
| @@ -1083,7 +1674,7 @@ async function duplicatePersona(avatarId) { | ||
| 1083 | 1674 | const personaName = power_user.personas[avatarId]; |
| 1084 | 1675 | |
| 1085 | 1676 | if (!personaName) { |
| 1086 | 1677 | toastr.warning('Chosen avatar is not a persona', t`Persona Management`); |
| 1087 | 1678 | return; |
| 1088 | 1679 | } |
| 1089 | 1680 | |
| @@ -1111,11 +1702,207 @@ async function duplicatePersona(avatarId) { | ||
| 1111 | 1702 | saveSettingsDebounced(); |
| 1112 | 1703 | } |
| 1113 | 1704 | |
| 1114 | -export function initPersonas() { | |
| 1705 | +/** | |
| 1115 | - $(document).on('click', '.bind_user_name', bindUserNameToPersona); | |
| 1706 | + * If a current user avatar is not bound to persona, bind it. | |
| 1116 | - $(document).on('click', '.set_default_persona', setDefaultPersona); | |
| 1707 | + */ | |
| 1117 | - $(document).on('click', '.delete_avatar', deleteUserAvatar); | |
| 1708 | +async function migrateNonPersonaUser() { | |
| 1118 | - $('#lock_user_name').on('click', togglePersonaLock); | |
| 1709 | + if (user_avatar in power_user.personas) { | |
| 1710 | + return; | |
| 1711 | + } | |
| 1712 | + | |
| 1713 | + initPersona(user_avatar, name1, ''); | |
| 1714 | + setPersonaDescription(); | |
| 1715 | + await getUserAvatars(true, user_avatar); | |
| 1716 | +} | |
| 1717 | + | |
| 1718 | + | |
| 1719 | +/** | |
| 1720 | + * Locks or unlocks the persona of the current chat. | |
| 1721 | + * @param {{type: string}} _args Named arguments | |
| 1722 | + * @param {string} value The value to set the lock to | |
| 1723 | + * @returns {Promise<string>} The value of the lock after setting | |
| 1724 | + */ | |
| 1725 | +async function lockPersonaCallback(_args, value) { | |
| 1726 | + const type = /** @type {PersonaLockType} */ (_args.type ?? 'chat'); | |
| 1727 | + | |
| 1728 | + if (!['chat', 'character', 'default'].includes(type)) { | |
| 1729 | + toastr.warning(t`Unknown lock type "${type}"`, t`Persona Management`); | |
| 1730 | + return ''; | |
| 1731 | + } | |
| 1732 | + | |
| 1733 | + if (!value) { | |
| 1734 | + return String(isPersonaLocked(type)); | |
| 1735 | + } | |
| 1736 | + | |
| 1737 | + if (['toggle', 't'].includes(value.trim().toLowerCase())) { | |
| 1738 | + const result = await togglePersonaLock(type); | |
| 1739 | + return String(result); | |
| 1740 | + } | |
| 1741 | + | |
| 1742 | + if (isTrueBoolean(value)) { | |
| 1743 | + await setPersonaLockState(true, type); | |
| 1744 | + return 'true'; | |
| 1745 | + } | |
| 1746 | + | |
| 1747 | + if (isFalseBoolean(value)) { | |
| 1748 | + await setPersonaLockState(false, type); | |
| 1749 | + return 'false'; | |
| 1750 | + | |
| 1751 | + } | |
| 1752 | + | |
| 1753 | + return ''; | |
| 1754 | +} | |
| 1755 | + | |
| 1756 | +/** | |
| 1757 | + * Sets a persona name and optionally an avatar. | |
| 1758 | + * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments | |
| 1759 | + * @param {string} name Name to set | |
| 1760 | + * @returns {string} | |
| 1761 | + */ | |
| 1762 | +function setNameCallback({ mode = 'all' }, name) { | |
| 1763 | + if (!name) { | |
| 1764 | + toastr.warning('You must specify a name to change to'); | |
| 1765 | + return ''; | |
| 1766 | + } | |
| 1767 | + | |
| 1768 | + if (!['lookup', 'temp', 'all'].includes(mode)) { | |
| 1769 | + toastr.warning('Mode must be one of "lookup", "temp" or "all"'); | |
| 1770 | + return ''; | |
| 1771 | + } | |
| 1772 | + | |
| 1773 | + name = name.trim(); | |
| 1774 | + | |
| 1775 | + // If the name matches a persona avatar, or a name, auto-select it | |
| 1776 | + if (['lookup', 'all'].includes(mode)) { | |
| 1777 | + let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; | |
| 1778 | + if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; | |
| 1779 | + if (persona) { | |
| 1780 | + autoSelectPersona(persona); | |
| 1781 | + retriggerFirstMessageOnEmptyChat(); | |
| 1782 | + return ''; | |
| 1783 | + } else if (mode === 'lookup') { | |
| 1784 | + toastr.warning(`Persona ${name} not found`); | |
| 1785 | + return ''; | |
| 1786 | + } | |
| 1787 | + } | |
| 1788 | + | |
| 1789 | + if (['temp', 'all'].includes(mode)) { | |
| 1790 | + // Otherwise, set just the name | |
| 1791 | + setUserName(name); //this prevented quickReply usage | |
| 1792 | + retriggerFirstMessageOnEmptyChat(); | |
| 1793 | + } | |
| 1794 | + | |
| 1795 | + return ''; | |
| 1796 | +} | |
| 1797 | + | |
| 1798 | +function syncCallback() { | |
| 1799 | + $('#sync_name_button').trigger('click'); | |
| 1800 | + return ''; | |
| 1801 | +} | |
| 1802 | + | |
| 1803 | +function registerPersonaSlashCommands() { | |
| 1804 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1805 | + name: 'persona-lock', | |
| 1806 | + callback: lockPersonaCallback, | |
| 1807 | + returns: 'The current lock state for the given type', | |
| 1808 | + helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.', | |
| 1809 | + namedArgumentList: [ | |
| 1810 | + SlashCommandNamedArgument.fromProps({ | |
| 1811 | + name: 'type', | |
| 1812 | + description: 'The type of the lock, where it should apply to', | |
| 1813 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1814 | + defaultValue: 'chat', | |
| 1815 | + enumList: [ | |
| 1816 | + new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'), | |
| 1817 | + new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'), | |
| 1818 | + new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'), | |
| 1819 | + ], | |
| 1820 | + }), | |
| 1821 | + ], | |
| 1822 | + unnamedArgumentList: [ | |
| 1823 | + SlashCommandArgument.fromProps({ | |
| 1824 | + description: 'state', | |
| 1825 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1826 | + enumProvider: commonEnumProviders.boolean('onOffToggle'), | |
| 1827 | + }), | |
| 1828 | + ], | |
| 1829 | + })); | |
| 1830 | + // TODO: Legacy command. Might be removed in the future and replaced by /persona-lock with aliases. | |
| 1831 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1832 | + name: 'lock', | |
| 1833 | + /** @type {(args: { type: string }, value: string) => Promise<string>} */ | |
| 1834 | + callback: (args, value) => { | |
| 1835 | + if (!value) { | |
| 1836 | + value = 'toggle'; | |
| 1837 | + toastr.warning(t`Using /lock without a provided state to toggle the persona is deprecated. Please use /persona-lock instead. | |
| 1838 | + In the future this command with no state provided will return the current state, instead of toggling it.`, t`Deprecation Warning`); | |
| 1839 | + } | |
| 1840 | + return lockPersonaCallback(args, value); | |
| 1841 | + }, | |
| 1842 | + returns: 'The current lock state for the given type', | |
| 1843 | + aliases: ['bind'], | |
| 1844 | + helpString: 'Locks/unlocks a persona (name and avatar) to the current chat. Gets the current lock state for the given type if no state is provided.', | |
| 1845 | + namedArgumentList: [ | |
| 1846 | + SlashCommandNamedArgument.fromProps({ | |
| 1847 | + name: 'type', | |
| 1848 | + description: 'The type of the lock, where it should apply to', | |
| 1849 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1850 | + defaultValue: 'chat', | |
| 1851 | + enumList: [ | |
| 1852 | + new SlashCommandEnumValue('chat', 'Lock the persona to the current chat.'), | |
| 1853 | + new SlashCommandEnumValue('character', 'Lock this persona to the currently selected character. If the setting is enabled, mutliple personas can be locked to the same character.'), | |
| 1854 | + new SlashCommandEnumValue('default', 'Lock this persona as the default persona for all new chats.'), | |
| 1855 | + ], | |
| 1856 | + }), | |
| 1857 | + ], | |
| 1858 | + unnamedArgumentList: [ | |
| 1859 | + SlashCommandArgument.fromProps({ | |
| 1860 | + description: 'state', | |
| 1861 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1862 | + defaultValue: 'toggle', | |
| 1863 | + enumProvider: commonEnumProviders.boolean('onOffToggle'), | |
| 1864 | + }), | |
| 1865 | + ], | |
| 1866 | + })); | |
| 1867 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1868 | + name: 'persona-set', | |
| 1869 | + callback: setNameCallback, | |
| 1870 | + aliases: ['persona', 'name'], | |
| 1871 | + namedArgumentList: [ | |
| 1872 | + new SlashCommandNamedArgument( | |
| 1873 | + 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', | |
| 1874 | + [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], | |
| 1875 | + ), | |
| 1876 | + ], | |
| 1877 | + unnamedArgumentList: [ | |
| 1878 | + SlashCommandArgument.fromProps({ | |
| 1879 | + description: 'persona name', | |
| 1880 | + typeList: [ARGUMENT_TYPE.STRING], | |
| 1881 | + isRequired: true, | |
| 1882 | + enumProvider: commonEnumProviders.personas, | |
| 1883 | + }), | |
| 1884 | + ], | |
| 1885 | + helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', | |
| 1886 | + })); | |
| 1887 | + SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 1888 | + name: 'persona-sync', | |
| 1889 | + aliases: ['sync'], | |
| 1890 | + callback: syncCallback, | |
| 1891 | + helpString: 'Syncs the user persona in user-attributed messages in the current chat.', | |
| 1892 | + })); | |
| 1893 | +} | |
| 1894 | + | |
| 1895 | +/** | |
| 1896 | + * Initializes the persona management and all its functionality. | |
| 1897 | + * This is called during the initialization of the page. | |
| 1898 | + */ | |
| 1899 | +export async function initPersonas() { | |
| 1900 | + await migrateNonPersonaUser(); | |
| 1901 | + registerPersonaSlashCommands(); | |
| 1902 | + $('#persona_delete_button').on('click', deleteUserAvatar); | |
| 1903 | + $('#lock_persona_default').on('click', () => togglePersonaLock('default')); | |
| 1904 | + $('#lock_user_name').on('click', () => togglePersonaLock('chat')); | |
| 1905 | + $('#lock_persona_to_char').on('click', () => togglePersonaLock('character')); | |
| 1119 | 1906 | $('#create_dummy_persona').on('click', createDummyPersona); |
| 1120 | 1907 | $('#persona_description').on('input', onPersonaDescriptionInput); |
| 1121 | 1908 | $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); |
| @@ -1151,55 +1938,41 @@ export function initPersonas() { | ||
| 1151 | 1938 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 1152 | 1939 | |
| 1153 | 1940 | $(document).on('click', '#user_avatar_block .avatar-container', function () { |
| 1154 | 1941 | const imgfile = $(this).attr('imgfiledata-avatar-id'); |
| 1155 | 1942 | setUserAvatar(imgfile); |
| 1156 | 1943 | |
| 1157 | 1944 | // force firstMes {{user}} update on persona switch |
| 1158 | 1945 | retriggerFirstMessageOnEmptyChat(); |
| 1159 | 1946 | }); |
| 1160 | 1947 | |
| 1161 | 1948 | $('#your_name_buttonpersona_rename_button').on('click', (async) function=> renamePersona(user_avatar) {); |
| 1162 | - const userName = String($('#your_name').val()).trim(); | |
| 1163 | - setUserName(userName); | |
| 1164 | - await updatePersonaNameIfExists(user_avatar, userName); | |
| 1165 | - retriggerFirstMessageOnEmptyChat(); | |
| 1166 | - }); | |
| 1167 | 1949 | |
| 1168 | 1950 | $(document).on('click', '#user_avatar_block .avatar_upload', function () { |
| 1169 | 1951 | $('#avatar_upload_overwrite').val(''); |
| 1170 | 1952 | $('#avatar_upload_file').trigger('click'); |
| 1171 | 1953 | }); |
| 1172 | 1954 | |
| 1173 | 1955 | $(document'#persona_duplicate_button').on('click', '#user_avatar_block .duplicate_persona',() function=> duplicatePersona(euser_avatar) {); |
| 1174 | - e.stopPropagation(); | |
| 1175 | - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | |
| 1176 | - | |
| 1177 | - if (!avatarId) { | |
| 1178 | - console.log('no imgfile'); | |
| 1179 | - return; | |
| 1180 | - } | |
| 1181 | - | |
| 1182 | - duplicatePersona(avatarId); | |
| 1183 | - }); | |
| 1184 | - | |
| 1185 | - $(document).on('click', '#user_avatar_block .set_persona_image', function (e) { | |
| 1186 | - e.stopPropagation(); | |
| 1187 | - const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | |
| 1188 | 1956 | |
| 1189 | - if (!avatarId) { | |
| 1957 | + $('#persona_set_image_button').on('click', function () { | |
| 1958 | + if (!user_avatar) { | |
| 1190 | 1959 | console.log('no imgfile'); |
| 1191 | 1960 | return; |
| 1192 | 1961 | } |
| 1193 | 1962 | |
| 1194 | 1963 | $('#avatar_upload_overwrite').val(avatarIduser_avatar); |
| 1195 | 1964 | $('#avatar_upload_file').trigger('click'); |
| 1196 | 1965 | }); |
| 1197 | 1966 | |
| 1967 | + $('#char_connections_button').on('click', showCharConnections); | |
| 1968 | + | |
| 1198 | 1969 | eventSource.on('charManagementDropdown', (target) => { |
| 1199 | 1970 | if (target === 'convert_to_persona') { |
| 1200 | 1971 | convertCharacterToPersona(); |
| 1201 | 1972 | } |
| 1202 | 1973 | }); |
| 1203 | 1974 | eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersonaupdatePersonaUIStates); |
| 1975 | + eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat); | |
| 1204 | 1976 | switchPersonaGridView(); |
| 1205 | 1977 | } |
| 1978 | + | |
| @@ -1489,6 +1489,8 @@ async function loadPowerUserSettings(settings, data) { | ||
| 1489 | 1489 | $('#custom_stopping_strings_macro').prop('checked', power_user.custom_stopping_strings_macro); |
| 1490 | 1490 | $('#fuzzy_search_checkbox').prop('checked', power_user.fuzzy_search); |
| 1491 | 1491 | $('#persona_show_notifications').prop('checked', power_user.persona_show_notifications); |
| 1492 | + $('#persona_allow_multi_connections').prop('checked', power_user.persona_allow_multi_connections); | |
| 1493 | + $('#persona_auto_lock').prop('checked', power_user.persona_auto_lock); | |
| 1492 | 1494 | $('#encode_tags').prop('checked', power_user.encode_tags); |
| 1493 | 1495 | $('#example_messages_behavior').val(getExampleMessagesBehavior()); |
| 1494 | 1496 | $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true); |
| @@ -3705,6 +3707,16 @@ $(document).ready(() => { | ||
| 3705 | 3707 | saveSettingsDebounced(); |
| 3706 | 3708 | }); |
| 3707 | 3709 | |
| 3710 | + $('#persona_allow_multi_connections').on('input', function () { | |
| 3711 | + power_user.persona_allow_multi_connections = !!$(this).prop('checked'); | |
| 3712 | + saveSettingsDebounced(); | |
| 3713 | + }); | |
| 3714 | + | |
| 3715 | + $('#persona_auto_lock').on('input', function () { | |
| 3716 | + power_user.persona_auto_lock = !!$(this).prop('checked'); | |
| 3717 | + saveSettingsDebounced(); | |
| 3718 | + }); | |
| 3719 | + | |
| 3708 | 3720 | $('#encode_tags').on('input', async function () { |
| 3709 | 3721 | power_user.encode_tags = !!$(this).prop('checked'); |
| 3710 | 3722 | await reloadCurrentChat(); |
| @@ -38,7 +38,6 @@ import { | ||
| 38 | 38 | setCharacterId, |
| 39 | 39 | setCharacterName, |
| 40 | 40 | setExtensionPrompt, |
| 41 | - setUserName, | |
| 42 | 41 | showMoreMessages, |
| 43 | 42 | stopGeneration, |
| 44 | 43 | substituteParams, |
| @@ -55,7 +54,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js'; | ||
| 55 | 54 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 56 | 55 | import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; |
| 57 | 56 | import { chat_completion_sources, oai_settings, promptManager } from './openai.js'; |
| 58 | 57 | import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js'; |
| 59 | 58 | import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; |
| 60 | 59 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 61 | 60 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; |
| @@ -124,46 +123,6 @@ export function initDefaultSlashCommands() { | ||
| 124 | 123 | helpString: 'Get help on macros, chat formatting and commands.', |
| 125 | 124 | })); |
| 126 | 125 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ |
| 127 | - name: 'persona', | |
| 128 | - callback: setNameCallback, | |
| 129 | - aliases: ['name'], | |
| 130 | - namedArgumentList: [ | |
| 131 | - new SlashCommandNamedArgument( | |
| 132 | - 'mode', 'The mode for persona selection. ("lookup" = search for existing persona, "temp" = create a temporary name, set a temporary name, "all" = allow both in the same command)', | |
| 133 | - [ARGUMENT_TYPE.STRING], false, false, 'all', ['lookup', 'temp', 'all'], | |
| 134 | - ), | |
| 135 | - ], | |
| 136 | - unnamedArgumentList: [ | |
| 137 | - SlashCommandArgument.fromProps({ | |
| 138 | - description: 'persona name', | |
| 139 | - typeList: [ARGUMENT_TYPE.STRING], | |
| 140 | - isRequired: true, | |
| 141 | - enumProvider: commonEnumProviders.personas, | |
| 142 | - }), | |
| 143 | - ], | |
| 144 | - helpString: 'Selects the given persona with its name and avatar (by name or avatar url). If no matching persona exists, applies a temporary name.', | |
| 145 | - })); | |
| 146 | - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 147 | - name: 'sync', | |
| 148 | - callback: syncCallback, | |
| 149 | - helpString: 'Syncs the user persona in user-attributed messages in the current chat.', | |
| 150 | - })); | |
| 151 | - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 152 | - name: 'lock', | |
| 153 | - callback: lockPersonaCallback, | |
| 154 | - aliases: ['bind'], | |
| 155 | - helpString: 'Locks/unlocks a persona (name and avatar) to the current chat', | |
| 156 | - unnamedArgumentList: [ | |
| 157 | - SlashCommandArgument.fromProps({ | |
| 158 | - description: 'state', | |
| 159 | - typeList: [ARGUMENT_TYPE.STRING], | |
| 160 | - isRequired: true, | |
| 161 | - defaultValue: 'toggle', | |
| 162 | - enumProvider: commonEnumProviders.boolean('onOffToggle'), | |
| 163 | - }), | |
| 164 | - ], | |
| 165 | - })); | |
| 166 | - SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | |
| 167 | 126 | name: 'bg', |
| 168 | 127 | callback: setBackgroundCallback, |
| 169 | 128 | aliases: ['background'], |
| @@ -3439,31 +3398,6 @@ export async function generateSystemMessage(_, prompt) { | ||
| 3439 | 3398 | return ''; |
| 3440 | 3399 | } |
| 3441 | 3400 | |
| 3442 | -function syncCallback() { | |
| 3443 | - $('#sync_name_button').trigger('click'); | |
| 3444 | - return ''; | |
| 3445 | -} | |
| 3446 | - | |
| 3447 | -async function lockPersonaCallback(_args, value) { | |
| 3448 | - if (['toggle', 't', ''].includes(value.trim().toLowerCase())) { | |
| 3449 | - await togglePersonaLock(); | |
| 3450 | - return ''; | |
| 3451 | - } | |
| 3452 | - | |
| 3453 | - if (isTrueBoolean(value)) { | |
| 3454 | - await setPersonaLockState(true); | |
| 3455 | - return ''; | |
| 3456 | - } | |
| 3457 | - | |
| 3458 | - if (isFalseBoolean(value)) { | |
| 3459 | - await setPersonaLockState(false); | |
| 3460 | - return ''; | |
| 3461 | - | |
| 3462 | - } | |
| 3463 | - | |
| 3464 | - return ''; | |
| 3465 | -} | |
| 3466 | - | |
| 3467 | 3401 | function setStoryModeCallback() { |
| 3468 | 3402 | $('#chat_display').val(chat_styles.DOCUMENT).trigger('change'); |
| 3469 | 3403 | return ''; |
| @@ -3479,48 +3413,6 @@ function setFlatModeCallback() { | ||
| 3479 | 3413 | return ''; |
| 3480 | 3414 | } |
| 3481 | 3415 | |
| 3482 | -/** | |
| 3483 | - * Sets a persona name and optionally an avatar. | |
| 3484 | - * @param {{mode: 'lookup' | 'temp' | 'all'}} namedArgs Named arguments | |
| 3485 | - * @param {string} name Name to set | |
| 3486 | - * @returns {string} | |
| 3487 | - */ | |
| 3488 | -function setNameCallback({ mode = 'all' }, name) { | |
| 3489 | - if (!name) { | |
| 3490 | - toastr.warning('You must specify a name to change to'); | |
| 3491 | - return ''; | |
| 3492 | - } | |
| 3493 | - | |
| 3494 | - if (!['lookup', 'temp', 'all'].includes(mode)) { | |
| 3495 | - toastr.warning('Mode must be one of "lookup", "temp" or "all"'); | |
| 3496 | - return ''; | |
| 3497 | - } | |
| 3498 | - | |
| 3499 | - name = name.trim(); | |
| 3500 | - | |
| 3501 | - // If the name matches a persona avatar, or a name, auto-select it | |
| 3502 | - if (['lookup', 'all'].includes(mode)) { | |
| 3503 | - let persona = Object.entries(power_user.personas).find(([avatar, _]) => avatar === name)?.[1]; | |
| 3504 | - if (!persona) persona = Object.entries(power_user.personas).find(([_, personaName]) => personaName.toLowerCase() === name.toLowerCase())?.[1]; | |
| 3505 | - if (persona) { | |
| 3506 | - autoSelectPersona(persona); | |
| 3507 | - retriggerFirstMessageOnEmptyChat(); | |
| 3508 | - return ''; | |
| 3509 | - } else if (mode === 'lookup') { | |
| 3510 | - toastr.warning(`Persona ${name} not found`); | |
| 3511 | - return ''; | |
| 3512 | - } | |
| 3513 | - } | |
| 3514 | - | |
| 3515 | - if (['temp', 'all'].includes(mode)) { | |
| 3516 | - // Otherwise, set just the name | |
| 3517 | - setUserName(name); //this prevented quickReply usage | |
| 3518 | - retriggerFirstMessageOnEmptyChat(); | |
| 3519 | - } | |
| 3520 | - | |
| 3521 | - return ''; | |
| 3522 | -} | |
| 3523 | - | |
| 3524 | 3416 | async function setNarratorName(_, text) { |
| 3525 | 3417 | const name = text || NARRATOR_NAME_DEFAULT; |
| 3526 | 3418 | chat_metadata[NARRATOR_NAME_KEY] = name; |
| @@ -485,8 +485,8 @@ export function getTagKeyForEntityElement(element) { | ||
| 485 | 485 | } |
| 486 | 486 | // Start with the given element and traverse up the DOM tree |
| 487 | 487 | while (element.length && element.parent().length) { |
| 488 | 488 | const grid = element.attr('data-grid'); |
| 489 | 489 | const chid = element.attr('data-chid'); |
| 490 | 490 | if (grid || chid) { |
| 491 | 491 | const id = grid || chid; |
| 492 | 492 | return getTagKeyForEntity(id); |
| @@ -2256,3 +2256,39 @@ export function arraysEqual(a, b) { | ||
| 2256 | 2256 | } |
| 2257 | 2257 | return true; |
| 2258 | 2258 | } |
| 2259 | + | |
| 2260 | +/** | |
| 2261 | + * Updates the content and style of an information block | |
| 2262 | + * @param {string | HTMLElement} target - The CSS selector or the HTML element of the information block | |
| 2263 | + * @param {string | HTMLElement?} content - The message to display inside the information block (supports HTML) or an HTML element | |
| 2264 | + * @param {'hint' | 'info' | 'warning' | 'error'} [type='info'] - The type of message, which determines the styling of the information block | |
| 2265 | + */ | |
| 2266 | +export function setInfoBlock(target, content, type = 'info') { | |
| 2267 | + if (!content) { | |
| 2268 | + clearInfoBlock(target); | |
| 2269 | + return; | |
| 2270 | + } | |
| 2271 | + | |
| 2272 | + const infoBlock = typeof target === 'string' ? document.querySelector(target) : target; | |
| 2273 | + if (infoBlock) { | |
| 2274 | + infoBlock.className = `info-block ${type}`; | |
| 2275 | + if (typeof content === 'string') { | |
| 2276 | + infoBlock.innerHTML = content; | |
| 2277 | + } else { | |
| 2278 | + infoBlock.innerHTML = ''; | |
| 2279 | + infoBlock.appendChild(content); | |
| 2280 | + } | |
| 2281 | + } | |
| 2282 | +} | |
| 2283 | + | |
| 2284 | +/** | |
| 2285 | + * Clears the content and style of an information block. | |
| 2286 | + * @param {string | HTMLElement} target - The CSS selector or the HTML element of the information block | |
| 2287 | + */ | |
| 2288 | +export function clearInfoBlock(target) { | |
| 2289 | + const infoBlock = typeof target === 'string' ? document.querySelector(target) : target; | |
| 2290 | + if (infoBlock && infoBlock.classList.contains('info-block')) { | |
| 2291 | + infoBlock.className = ''; | |
| 2292 | + infoBlock.innerHTML = ''; | |
| 2293 | + } | |
| 2294 | +} | |
| @@ -4749,7 +4749,7 @@ export function checkEmbeddedWorld(chid) { | ||
| 4749 | 4749 | } |
| 4750 | 4750 | |
| 4751 | 4751 | if (characters[chid]?.data?.character_book) { |
| 4752 | 4752 | $('#import_character_info').data('data-chid', chid).show(); |
| 4753 | 4753 | |
| 4754 | 4754 | // Only show the alert once per character |
| 4755 | 4755 | const checkKey = `AlertWI_${characters[chid].avatar}`; |
| @@ -4783,7 +4783,7 @@ export function checkEmbeddedWorld(chid) { | ||
| 4783 | 4783 | } |
| 4784 | 4784 | |
| 4785 | 4785 | export async function importEmbeddedWorldInfo(skipPopup = false) { |
| 4786 | 4786 | const chid = $('#import_character_info').data('data-chid'); |
| 4787 | 4787 | |
| 4788 | 4788 | if (chid === undefined) { |
| 4789 | 4789 | return; |
| @@ -228,6 +228,37 @@ table.responsiveTable { | ||
| 228 | 228 | animation-name: flash; |
| 229 | 229 | } |
| 230 | 230 | |
| 231 | +.has_hover_label .label_icon { | |
| 232 | + transition: opacity var(--animation-duration) ease, max-width var(--animation-duration) ease; | |
| 233 | +} | |
| 234 | +.has_hover_label .label { | |
| 235 | + transition: opacity var(--animation-duration-slow) ease, max-width var(--animation-duration-slow) ease; | |
| 236 | + /* Prevent double gap on hidden icon */ | |
| 237 | + margin-left: -5px; | |
| 238 | +} | |
| 239 | + | |
| 240 | +.has_hover_label .label_icon, | |
| 241 | +.has_hover_label .label { | |
| 242 | + transition-delay: var(--animation-duration-slow); | |
| 243 | +} | |
| 244 | +.has_hover_label.fast .label_icon, | |
| 245 | +.has_hover_label.fast .label { | |
| 246 | + transition-delay: var(--animation-duration); | |
| 247 | +} | |
| 248 | + | |
| 249 | +.has_hover_label .label_icon, | |
| 250 | +.has_hover_label:hover .label { | |
| 251 | + opacity: 1; | |
| 252 | + max-width: 100px; | |
| 253 | +} | |
| 254 | + | |
| 255 | +.has_hover_label:hover .label_icon, | |
| 256 | +.has_hover_label .label { | |
| 257 | + opacity: 0; | |
| 258 | + max-width: 0; | |
| 259 | + overflow: hidden; | |
| 260 | +} | |
| 261 | + | |
| 231 | 262 | /* Keyboard/focus navigation styling */ |
| 232 | 263 | /* Mimic the outline of keyboard navigation for most most focusable controls */ |
| 233 | 264 | .interactable { |
| @@ -1339,6 +1370,8 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { | ||
| 1339 | 1370 | } |
| 1340 | 1371 | |
| 1341 | 1372 | .avatars_inline { |
| 1373 | + display: flex; | |
| 1374 | + gap: 5px; | |
| 1342 | 1375 | flex-wrap: wrap; |
| 1343 | 1376 | overflow: hidden; |
| 1344 | 1377 | max-height: calc(var(--avatar-base-height) + 2 * var(--avatar-base-border-radius)); |
| @@ -1375,6 +1408,11 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { | ||
| 1375 | 1408 | margin-right: calc(var(--avatar-base-border-radius)); |
| 1376 | 1409 | } |
| 1377 | 1410 | |
| 1411 | +.avatars_multiline { | |
| 1412 | + max-height: fit-content; | |
| 1413 | + height: inherit; | |
| 1414 | +} | |
| 1415 | + | |
| 1378 | 1416 | .mes_block { |
| 1379 | 1417 | padding-top: 0; |
| 1380 | 1418 | padding-left: 10px; |
| @@ -2869,6 +2907,7 @@ select option:not(:checked) { | ||
| 2869 | 2907 | .menu_button.disabled { |
| 2870 | 2908 | filter: brightness(75%) grayscale(1); |
| 2871 | 2909 | opacity: 0.5; |
| 2910 | + cursor: inherit; | |
| 2872 | 2911 | pointer-events: none; |
| 2873 | 2912 | } |
| 2874 | 2913 | |
| @@ -2943,8 +2982,7 @@ select option:not(:checked) { | ||
| 2943 | 2982 | } |
| 2944 | 2983 | |
| 2945 | 2984 | #form_character_search_form .menu_button, |
| 2946 | 2985 | #GroupFavDelOkBack .menu_button, { |
| 2947 | -.avatar-container .menu_button { | |
| 2948 | 2986 | margin: 0; |
| 2949 | 2987 | height: fit-content; |
| 2950 | 2988 | padding: 5px; |
| @@ -2978,8 +3016,22 @@ select option:not(:checked) { | ||
| 2978 | 3016 | width: max-content; |
| 2979 | 3017 | } |
| 2980 | 3018 | |
| 2981 | 3019 | #persona-management-block .avatar_container_states .menu_button { |
| 2982 | - filter: grayscale(0.5); | |
| 3020 | + padding: 3px 5px; | |
| 3021 | + pointer-events: initial; | |
| 3022 | +} | |
| 3023 | + | |
| 3024 | +#persona_controls .persona_name { | |
| 3025 | + text-overflow: ellipsis; | |
| 3026 | + overflow: hidden; | |
| 3027 | + text-align: left; | |
| 3028 | + white-space: nowrap; | |
| 3029 | + font-size: calc(var(--mainFontSize) * 1.25); | |
| 3030 | + opacity: 0.5; | |
| 3031 | +} | |
| 3032 | + | |
| 3033 | +#persona_connections_buttons { | |
| 3034 | + margin-bottom: 5px; | |
| 2983 | 3035 | } |
| 2984 | 3036 | |
| 2985 | 3037 | input[type=search]::-webkit-search-cancel-button { |
| @@ -3304,6 +3356,10 @@ input[type=search]:focus::-webkit-search-cancel-button { | ||
| 3304 | 3356 | color: var(--golden); |
| 3305 | 3357 | } |
| 3306 | 3358 | |
| 3359 | +.avatar.is_active { | |
| 3360 | + outline: 2px solid var(--active); | |
| 3361 | +} | |
| 3362 | + | |
| 3307 | 3363 | #fav_chara_wrap { |
| 3308 | 3364 | display: flex; |
| 3309 | 3365 | margin: 5px 0px; |
| @@ -3371,28 +3427,6 @@ grammarly-extension { | ||
| 3371 | 3427 | z-index: 35; |
| 3372 | 3428 | } |
| 3373 | 3429 | |
| 3374 | -.avatar-container .avatar-buttons { | |
| 3375 | - display: flex; | |
| 3376 | - flex-direction: row; | |
| 3377 | - gap: 3px; | |
| 3378 | - opacity: 0.3; | |
| 3379 | - transition: opacity 0.25s ease-in-out; | |
| 3380 | -} | |
| 3381 | - | |
| 3382 | -.avatar-container .avatar-buttons:hover { | |
| 3383 | - opacity: 1; | |
| 3384 | -} | |
| 3385 | - | |
| 3386 | -.avatar-container .avatar-buttons .menu_button { | |
| 3387 | - padding: 3px; | |
| 3388 | -} | |
| 3389 | - | |
| 3390 | -/* Ross should be able to handle this later */ | |
| 3391 | -/*.big-avatars .avatar-buttons{ | |
| 3392 | - justify-content: center; | |
| 3393 | - width: fit-content; | |
| 3394 | -}*/ | |
| 3395 | - | |
| 3396 | 3430 | .avatar_div .avatar { |
| 3397 | 3431 | /* margin-left: 4px; |
| 3398 | 3432 | margin-right: 10px; |
| @@ -3455,12 +3489,7 @@ grammarly-extension { | ||
| 3455 | 3489 | cursor: pointer; |
| 3456 | 3490 | } |
| 3457 | 3491 | |
| 3458 | 3492 | #rm_characters_block .form_create_bottom_buttons_blockbuttons_block { |
| 3459 | - justify-content: space-evenly !important; | |
| 3460 | - flex-grow: 0; | |
| 3461 | -} | |
| 3462 | - | |
| 3463 | -.form_create_bottom_buttons_block { | |
| 3464 | 3493 | display: flex; |
| 3465 | 3494 | flex: 1; |
| 3466 | 3495 | gap: 5px; |
| @@ -3469,18 +3498,21 @@ grammarly-extension { | ||
| 3469 | 3498 | flex-wrap: wrap; |
| 3470 | 3499 | } |
| 3471 | 3500 | |
| 3472 | 3501 | .form_create_bottom_buttons_blockbuttons_block .menu_button { |
| 3473 | 3502 | display: flex; |
| 3474 | 3503 | justify-content: center; |
| 3475 | 3504 | align-items: center; |
| 3505 | + padding: 5px; | |
| 3506 | + margin: 0; | |
| 3507 | + filter: grayscale(0.5); | |
| 3476 | 3508 | } |
| 3477 | 3509 | |
| 3478 | -#delete_button, | |
| 3510 | +.red_button, | |
| 3479 | 3511 | .redWarningBG { |
| 3480 | 3512 | background-color: var(--crimson70a) !important; |
| 3481 | 3513 | } |
| 3482 | 3514 | |
| 3483 | 3515 | #delete_button.red_button:hover, |
| 3484 | 3516 | .redWarningBG:hover { |
| 3485 | 3517 | background-color: var(--fullred) !important; |
| 3486 | 3518 | } |
| @@ -3655,6 +3687,7 @@ grammarly-extension { | ||
| 3655 | 3687 | |
| 3656 | 3688 | .menu_button { |
| 3657 | 3689 | color: var(--SmartThemeBodyColor); |
| 3690 | + filter: grayscale(0.5); | |
| 3658 | 3691 | background-color: var(--SmartThemeBlurTintColor); |
| 3659 | 3692 | border: 1px solid var(--SmartThemeBorderColor); |
| 3660 | 3693 | border-radius: 5px; |
| @@ -3674,15 +3707,8 @@ grammarly-extension { | ||
| 3674 | 3707 | min-width: calc(1.25em + 12px); |
| 3675 | 3708 | } |
| 3676 | 3709 | |
| 3677 | -.avatar_div .menu_button, | |
| 3710 | +.menu_button:not(.disabled):hover, | |
| 3678 | -.form_create_bottom_buttons_block .menu_button { | |
| 3711 | +.menu_button:not(.disabled).active { | |
| 3679 | - padding: 5px; | |
| 3680 | - margin: 0; | |
| 3681 | - filter: grayscale(0.5); | |
| 3682 | -} | |
| 3683 | - | |
| 3684 | -.menu_button:hover, | |
| 3685 | -.menu_button.active { | |
| 3686 | 3712 | background-color: var(--white30a); |
| 3687 | 3713 | } |
| 3688 | 3714 | |
| @@ -3931,10 +3957,39 @@ input[type='checkbox'].del_checkbox { | ||
| 3931 | 3957 | outline: var(--avatar-base-border-radius) solid var(--golden); |
| 3932 | 3958 | } |
| 3933 | 3959 | |
| 3934 | 3960 | .avatar-container .default_personaavatar_state .set_default_personafa-lock { |
| 3961 | + color: var(--active); | |
| 3962 | +} | |
| 3963 | + | |
| 3964 | +.avatar-container:not(.locked_to_chat) .locked_to_chat_label { | |
| 3965 | + display: none; | |
| 3966 | +} | |
| 3967 | +.avatar-container:not(.locked_to_character) .locked_to_character_label { | |
| 3968 | + display: none; | |
| 3969 | +} | |
| 3970 | + | |
| 3971 | +#lock_persona_default.locked i.icon { | |
| 3935 | 3972 | color: var(--golden); |
| 3936 | 3973 | } |
| 3937 | 3974 | |
| 3975 | +#lock_user_name.locked .icon, | |
| 3976 | +.avatar-container.locked_to_chat .locked_to_chat_label .icon { | |
| 3977 | + color: var(--SmartThemeQuoteColor); | |
| 3978 | +} | |
| 3979 | + | |
| 3980 | +#lock_persona_to_char.locked .icon, | |
| 3981 | +.avatar-container.locked_to_character .locked_to_character_label .icon { | |
| 3982 | + color: var(--active); | |
| 3983 | +} | |
| 3984 | + | |
| 3985 | +#lock_user_name.locked { | |
| 3986 | + border-color: color-mix(in srgb, var(--SmartThemeQuoteColor) 50%, var(--SmartThemeBorderColor));; | |
| 3987 | +} | |
| 3988 | + | |
| 3989 | +#lock_persona_to_char.locked { | |
| 3990 | + border-color: color-mix(in srgb, var(--active) 50%, var(--SmartThemeBorderColor)); | |
| 3991 | +} | |
| 3992 | + | |
| 3938 | 3993 | #user_avatar_block .avatar_upload { |
| 3939 | 3994 | cursor: pointer; |
| 3940 | 3995 | width: 60px; |
| @@ -5958,3 +6013,40 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 5958 | 6013 | min-width: fit-content; |
| 5959 | 6014 | width: 40%; |
| 5960 | 6015 | } |
| 6016 | + | |
| 6017 | +.multiline { | |
| 6018 | + white-space: pre-wrap; | |
| 6019 | +} | |
| 6020 | + | |
| 6021 | +.info-block { | |
| 6022 | + padding: 10px 1em; | |
| 6023 | + margin: 1em 0; | |
| 6024 | + border-radius: 10px; | |
| 6025 | + border-left: 5px solid; | |
| 6026 | + background-color: var(--SmartThemeBlurTintColor); | |
| 6027 | + color: var(--SmartThemeBodyColor); | |
| 6028 | + backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); | |
| 6029 | + -webkit-backdrop-filter: blur(calc(var(--SmartThemeBlurStrength))); | |
| 6030 | +} | |
| 6031 | + | |
| 6032 | +.info-block.hint { | |
| 6033 | + border-color: var(--info-color, #7fb3e8); | |
| 6034 | + background-color: rgba(163, 201, 241, 0.2); | |
| 6035 | +} | |
| 6036 | + | |
| 6037 | +.info-block.info { | |
| 6038 | + border-color: var(--default-color, #e8e07f); | |
| 6039 | + background-color: rgba(255, 255, 224, 0.2); | |
| 6040 | +} | |
| 6041 | + | |
| 6042 | +.info-block.warning { | |
| 6043 | + border-color: var(--warning-color, #e8a97f); | |
| 6044 | + background-color: rgba(241, 198, 163, 0.2); | |
| 6045 | + /* Reset the font weight from that main warning class */ | |
| 6046 | + font-weight: unset; | |
| 6047 | +} | |
| 6048 | + | |
| 6049 | +.info-block.error { | |
| 6050 | + border-color: var(--error-color, #e87f7f); | |
| 6051 | + background-color: rgba(241, 163, 163, 0.2); | |
| 6052 | +} | |