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 | flex-direction: column; | 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 | body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container, | 99 | body.charListGrid #rm_print_characters_block .bogus_folder_select .character_select_container, |
| 105 | body.charListGrid #rm_print_characters_block .character_select .character_select_container, | 100 | body.charListGrid #rm_print_characters_block .character_select .character_select_container, |
| 106 | body.charListGrid #rm_print_characters_block .group_select .group_select_container, | 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 | body.big-avatars .avatars_inline { | 226 | body.big-avatars .avatars_inline { |
| 232 | max-height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) + 2 * var(--avatar-base-border-radius)); | 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 | body.big-avatars .avatars_inline.avatars_inline_small { | 233 | body.big-avatars .avatars_inline.avatars_inline_small { |
| 236 | height: calc(var(--avatar-base-height) * var(--big-avatar-height-factor) * var(--inline-avatar-small-factor) + 2 * var(--avatar-base-border-radius)); | 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 | body:not(.big-avatars) .avatars_inline_small .avatar_collage { | 240 | body:not(.big-avatars) .avatars_inline_small .avatar_collage { |
| 240 | min-width: calc(var(--avatar-base-width) * var(--inline-avatar-small-factor)); | 241 | min-width: calc(var(--avatar-base-width) * var(--inline-avatar-small-factor)); |
| @@ -4987,7 +4987,7 @@ | |||
| 4987 | </div> | 4987 | </div> |
| 4988 | </div> | 4988 | </div> |
| 4989 | <div id="persona-management-block" class="flex-container wide100p flexGap10"> | 4989 | <div id="persona-management-block" class="flex-container wide100p flexGap10"> |
| 4990 | <div class="flex1 overflowHidden wide100p"> | 4990 | <div class="persona_management_left_column flex1 overflowHidden wide100p"> |
| 4991 | <div class="flex-container marginBot10 alignitemscenter"> | 4991 | <div class="flex-container marginBot10 alignitemscenter"> |
| 4992 | <div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona" data-i18n="[title]Create a dummy persona"> | 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 | <i class="fa-solid fa-person-circle-question fa-fw"></i> | 4993 | <i class="fa-solid fa-person-circle-question fa-fw"></i> |
| @@ -5010,27 +5010,34 @@ | |||
| 5010 | <input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value=""> | 5010 | <input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value=""> |
| 5011 | </form> | 5011 | </form> |
| 5012 | </div> | 5012 | </div> |
| 5013 | <div class="flex1"> | 5013 | <div class="persona_management_right_column flex1"> |
| 5014 | <h4 data-i18n="Name">Name</h4> | 5014 | <div class="persona_management_current_persona"> |
| 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 | <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"> | 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> | ||
| 5024 | </div> | 5027 | </div> |
| 5025 | </div> | 5028 | </div> |
| 5026 | <div> | 5029 | |
| 5027 | <h4 data-i18n="Persona Description">Persona Description</h4> | 5030 | <h4 data-i18n="Persona Description">Persona Description</h4> |
| 5028 | <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> | 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 | |
| 5033 | <div class="flex-container justifySpaceBetween"> | ||
| 5034 | <h4 data-i18n="Position">Position</h4> | ||
| 5035 | <div class="extension_token_counter widthFitContent"> | ||
| 5030 | <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span> | 5036 | <span data-i18n="Tokens persona description">Tokens</span>: <span id="persona_description_token_count">0</span> |
| 5031 | </div> | 5037 | </div> |
| 5032 | <div> | 5038 | </div> |
| 5033 | <label for="persona_description_position" data-i18n="Position:">Position:</label> | 5039 | |
| 5040 | <div class="persona_management_description_position_container"> | ||
| 5034 | <select id="persona_description_position"> | 5041 | <select id="persona_description_position"> |
| 5035 | <option value="9" data-i18n="None (disabled)">None (disabled)</option> | 5042 | <option value="9" data-i18n="None (disabled)">None (disabled)</option> |
| 5036 | <option value="0" data-i18n="In Story String / Prompt Manager">In Story String / Prompt Manager</option> | 5043 | <option value="0" data-i18n="In Story String / Prompt Manager">In Story String / Prompt Manager</option> |
| @@ -5053,7 +5060,30 @@ | |||
| 5053 | </div> | 5060 | </div> |
| 5054 | </div> | 5061 | </div> |
| 5055 | </div> | 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 | </div> | 5082 | </div> |
| 5083 | |||
| 5084 | <div class="persona_management_global_settings"> | ||
| 5085 | <h4 class="standoutHeader" data-i18n="Global Settings">Global Settings</h4> | ||
| 5086 | |||
| 5057 | <div class="range-block"> | 5087 | <div class="range-block"> |
| 5058 | <label for="persona_show_notifications" class="checkbox_label"> | 5088 | <label for="persona_show_notifications" class="checkbox_label"> |
| 5059 | <input id="persona_show_notifications" type="checkbox" /> | 5089 | <input id="persona_show_notifications" type="checkbox" /> |
| @@ -5062,6 +5092,23 @@ | |||
| 5062 | </span> | 5092 | </span> |
| 5063 | </label> | 5093 | </label> |
| 5064 | </div> | 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> | ||
| 5111 | </div> | ||
| 5065 | </div> | 5112 | </div> |
| 5066 | </div> | 5113 | </div> |
| 5067 | </div> | 5114 | </div> |
| @@ -5087,7 +5134,7 @@ | |||
| 5087 | <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> | 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 | </div> | 5135 | </div> |
| 5089 | <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p"> | 5136 | <div id="HotSwapWrapper" class="alignitemscenter flex-container margin0auto wide100p"> |
| 5090 | <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> | 5137 | <div class="hotswap avatars_inline 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 | </div> | 5138 | </div> |
| 5092 | </div> | 5139 | </div> |
| 5093 | <hr> | 5140 | <hr> |
| @@ -5125,13 +5172,13 @@ | |||
| 5125 | </div> | 5172 | </div> |
| 5126 | </div> | 5173 | </div> |
| 5127 | <div class="flex-container flexFlowColumn expander flexNoGap"> | 5174 | <div class="flex-container flexFlowColumn expander flexNoGap"> |
| 5128 | <div id="avatar_div" class="avatar_div alignitemsflexstart justifySpaceBetween flexnowrap"> | 5175 | <div id="avatar_div" class="avatar_div buttons_block alignitemsflexstart justifySpaceBetween flexnowrap"> |
| 5129 | <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"> | 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 | <img id="avatar_load_preview" src="img/ai4.png" alt="avatar"> | 5177 | <img id="avatar_load_preview" src="img/ai4.png" alt="avatar"> |
| 5131 | <input hidden type="file" id="add_avatar_button" name="avatar" accept="image/*"> | 5178 | <input hidden type="file" id="add_avatar_button" name="avatar" accept="image/*"> |
| 5132 | </label> | 5179 | </label> |
| 5133 | <div class="flex-container" id="avatar_controls"> | 5180 | <div class="flex-container" id="avatar_controls"> |
| 5134 | <div class="form_create_bottom_buttons_block"> | 5181 | <div class="form_create_bottom_buttons_block buttons_block"> |
| 5135 | <div id="rm_button_back" class="menu_button fa-solid fa-left-long "></div> | 5182 | <div id="rm_button_back" class="menu_button fa-solid fa-left-long "></div> |
| 5136 | <!-- <div id="renameCharButton" class="menu_button fa-solid fa-user-pen" title="Rename Character"></div> --> | 5183 | <!-- <div id="renameCharButton" class="menu_button fa-solid fa-user-pen" title="Rename Character"></div> --> |
| 5137 | <div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites" data-i18n="[title]Add to Favorites"></div> | 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 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> | 5186 | <div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions" data-i18n="[title]Advanced Definition"></div> |
| 5140 | <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> | 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 | <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> | 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 | <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download" data-i18n="[title]Export and Download"></div> | 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 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> | 5191 | <!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> --> |
| 5144 | <!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> --> | 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 | <label for="create_button" id="create_button_label" class="menu_button fa-solid fa-user-check" title="Create Character" data-i18n="[title]Create Character"> | 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 | <input type="submit" id="create_button" name="create_button"> | 5195 | <input type="submit" id="create_button" name="create_button"> |
| 5148 | </label> | 5196 | </label> |
| 5149 | <div id="delete_button" class="menu_button fa-solid fa-skull " title="Delete Character" data-i18n="[title]Delete Character"></div> | 5197 | <div id="delete_button" class="menu_button fa-solid fa-skull red_button" title="Delete Character" data-i18n="[title]Delete Character"></div> |
| 5150 | </div> | 5198 | </div> |
| 5151 | <label class="flex1 height100p" for="char-management-dropdown"> | 5199 | <label class="flex1 height100p" for="char-management-dropdown"> |
| 5152 | <select id="char-management-dropdown" class="text_pole"> | 5200 | <select id="char-management-dropdown" class="text_pole"> |
| @@ -5483,25 +5531,20 @@ | |||
| 5483 | <div class="flex-container wide100pLess70px character_select_container"> | 5531 | <div class="flex-container wide100pLess70px character_select_container"> |
| 5484 | <div class="wide100p character_name_block"> | 5532 | <div class="wide100p character_name_block"> |
| 5485 | <span class="ch_name flex1"></span> | 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 | </div> | 5534 | </div> |
| 5504 | <div class="ch_description"></div> | 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 | </div> | 5548 | </div> |
| 5506 | </div> | 5549 | </div> |
| 5507 | </div> | 5550 | </div> |
| @@ -235,6 +235,8 @@ import { | |||
| 235 | initPersonas, | 235 | initPersonas, |
| 236 | setPersonaDescription, | 236 | setPersonaDescription, |
| 237 | initUserAvatar, | 237 | initUserAvatar, |
| 238 | updatePersonaConnectionsAvatarList, | ||
| 239 | isPersonaPanelOpen, | ||
| 238 | } from './scripts/personas.js'; | 240 | } from './scripts/personas.js'; |
| 239 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; | 241 | import { getBackgrounds, initBackgrounds, loadBackgroundSettings, background_settings } from './scripts/backgrounds.js'; |
| 240 | import { hideLoader, showLoader } from './scripts/loader.js'; | 242 | import { hideLoader, showLoader } from './scripts/loader.js'; |
| @@ -983,7 +985,7 @@ async function firstLoadInit() { | |||
| 983 | await initTokenizers(); | 985 | await initTokenizers(); |
| 984 | initBackgrounds(); | 986 | initBackgrounds(); |
| 985 | initAuthorsNote(); | 987 | initAuthorsNote(); |
| 986 | initPersonas(); | 988 | await initPersonas(); |
| 987 | initRossMods(); | 989 | initRossMods(); |
| 988 | initStats(); | 990 | initStats(); |
| 989 | initCfg(); | 991 | initCfg(); |
| @@ -1362,6 +1364,14 @@ export function resultCheckStatus() { | |||
| 1362 | stopStatusLoading(); | 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 | export async function selectCharacterById(id) { | 1375 | export async function selectCharacterById(id) { |
| 1366 | if (characters[id] === undefined) { | 1376 | if (characters[id] === undefined) { |
| 1367 | return; | 1377 | return; |
| @@ -1435,7 +1445,7 @@ function getCharacterBlock(item, id) { | |||
| 1435 | } | 1445 | } |
| 1436 | // Populate the template | 1446 | // Populate the template |
| 1437 | const template = $('#character_template .character_select').clone(); | 1447 | const template = $('#character_template .character_select').clone(); |
| 1438 | template.attr({ 'chid': id, 'id': `CharID${id}` }); | 1448 | template.attr({ 'data-chid': id, 'id': `CharID${id}` }); |
| 1439 | template.find('img').attr('src', this_avatar).attr('alt', item.name); | 1449 | template.find('img').attr('src', this_avatar).attr('alt', item.name); |
| 1440 | template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`); | 1450 | template.find('.avatar').attr('title', `[Character] ${item.name}\nFile: ${item.avatar}`); |
| 1441 | template.find('.ch_name').text(item.name).attr('title', `[Character] ${item.name}`); | 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 | favsToHotswap(); | 1573 | favsToHotswap(); |
| 1574 | updatePersonaConnectionsAvatarList(); | ||
| 1564 | } | 1575 | } |
| 1565 | 1576 | ||
| 1566 | /** Checks the state of the current search, and adds/removes the search sorting option accordingly */ | 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 | if (newChId !== -1) { | 6342 | if (newChId !== -1) { |
| 6332 | // Select the character after the renaming | 6343 | // Select the character after the renaming |
| 6333 | this_chid = -1; | 6344 | this_chid = -1; |
| 6334 | await selectCharacterById(String(newChId)); | 6345 | await selectCharacterById(newChId); |
| 6335 | 6346 | ||
| 6336 | // Async delay to update UI | 6347 | // Async delay to update UI |
| 6337 | await delay(1); | 6348 | await delay(1); |
| @@ -6608,7 +6619,7 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | |||
| 6608 | } | 6619 | } |
| 6609 | 6620 | ||
| 6610 | avatarTemplate.attr('data-type', entity.type); | 6621 | avatarTemplate.attr('data-type', entity.type); |
| 6611 | avatarTemplate.attr({ 'chid': id, 'id': `CharID${id}` }); | 6622 | avatarTemplate.attr('data-chid', id); |
| 6612 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); | 6623 | avatarTemplate.find('img').attr('src', this_avatar).attr('alt', entity.item.name); |
| 6613 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); | 6624 | avatarTemplate.attr('title', `[Character] ${entity.item.name}\nFile: ${entity.item.avatar}`); |
| 6614 | if (highlightFavs) { | 6625 | if (highlightFavs) { |
| @@ -6623,8 +6634,14 @@ export function buildAvatarList(block, entities, { templateId = 'inline_avatar_t | |||
| 6623 | avatarTemplate.addClass(grpTemplate.attr('class')); | 6634 | avatarTemplate.addClass(grpTemplate.attr('class')); |
| 6624 | avatarTemplate.empty(); | 6635 | avatarTemplate.empty(); |
| 6625 | avatarTemplate.append(grpTemplate.children()); | 6636 | avatarTemplate.append(grpTemplate.children()); |
| 6637 | avatarTemplate.attr({ 'data-grid': id, 'data-chid': null }); | ||
| 6626 | avatarTemplate.attr('title', `[Group] ${entity.item.name}`); | 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 | if (interactable) { | 6646 | if (interactable) { |
| 6630 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); | 6647 | avatarTemplate.addClass(INTERACTABLE_CONTROL_CLASS); |
| @@ -6861,14 +6878,14 @@ export function changeMainAPI() { | |||
| 6861 | forceCharacterEditorTokenize(); | 6878 | forceCharacterEditorTokenize(); |
| 6862 | } | 6879 | } |
| 6863 | 6880 | ||
| 6864 | export function setUserName(value) { | 6881 | export function setUserName(value, { toastPersonaNameChange = true } = {}) { |
| 6865 | name1 = value; | 6882 | name1 = value; |
| 6866 | if (name1 === undefined || name1 == '') | 6883 | if (name1 === undefined || name1 == '') |
| 6867 | name1 = default_user_name; | 6884 | name1 = default_user_name; |
| 6868 | console.log(`User name changed to ${name1}`); | 6885 | console.log(`User name changed to ${name1}`); |
| 6869 | $('#your_name').val(name1); | 6886 | $('#your_name').text(name1); |
| 6870 | if (power_user.persona_show_notifications) { | 6887 | if (toastPersonaNameChange && power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 6871 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Current persona updated`); | 6888 | toastr.success(t`Your messages will now be sent as ${name1}`, t`Persona Changed`); |
| 6872 | } | 6889 | } |
| 6873 | saveSettingsDebounced(); | 6890 | saveSettingsDebounced(); |
| 6874 | } | 6891 | } |
| @@ -6920,7 +6937,7 @@ export async function getSettings() { | |||
| 6920 | settings = JSON.parse(data.settings); | 6937 | settings = JSON.parse(data.settings); |
| 6921 | if (settings.username !== undefined && settings.username !== '') { | 6938 | if (settings.username !== undefined && settings.username !== '') { |
| 6922 | name1 = settings.username; | 6939 | name1 = settings.username; |
| 6923 | $('#your_name').val(name1); | 6940 | $('#your_name').text(name1); |
| 6924 | } | 6941 | } |
| 6925 | 6942 | ||
| 6926 | accountStorage.init(settings?.accountStorage); | 6943 | accountStorage.init(settings?.accountStorage); |
| @@ -10054,7 +10071,7 @@ jQuery(async function () { | |||
| 10054 | }); | 10071 | }); |
| 10055 | 10072 | ||
| 10056 | $(document).on('click', '.character_select', async function () { | 10073 | $(document).on('click', '.character_select', async function () { |
| 10057 | const id = $(this).attr('chid'); | 10074 | const id = Number($(this).attr('data-chid')); |
| 10058 | await selectCharacterById(id); | 10075 | await selectCharacterById(id); |
| 10059 | }); | 10076 | }); |
| 10060 | 10077 | ||
| @@ -672,7 +672,7 @@ class BulkEditOverlay { | |||
| 672 | * @param {HTMLElement} currentCharacter - The html element of the currently toggled character | 672 | * @param {HTMLElement} currentCharacter - The html element of the currently toggled character |
| 673 | */ | 673 | */ |
| 674 | handleShiftClick = (currentCharacter) => { | 674 | handleShiftClick = (currentCharacter) => { |
| 675 | const characterId = currentCharacter.getAttribute('chid'); | 675 | const characterId = Number(currentCharacter.getAttribute('data-chid')); |
| 676 | const select = !this.selectedCharacters.includes(characterId); | 676 | const select = !this.selectedCharacters.includes(characterId); |
| 677 | 677 | ||
| 678 | if (this.lastSelected.characterId && this.lastSelected.select !== undefined) { | 678 | if (this.lastSelected.characterId && this.lastSelected.select !== undefined) { |
| @@ -691,7 +691,7 @@ class BulkEditOverlay { | |||
| 691 | * @param {boolean} [param1.markState] - Whether the toggle of this character should be remembered as the last done toggle | 691 | * @param {boolean} [param1.markState] - Whether the toggle of this character should be remembered as the last done toggle |
| 692 | */ | 692 | */ |
| 693 | toggleSingleCharacter = (character, { markState = true } = {}) => { | 693 | toggleSingleCharacter = (character, { markState = true } = {}) => { |
| 694 | const characterId = character.getAttribute('chid'); | 694 | const characterId = Number(character.getAttribute('data-chid')); |
| 695 | 695 | ||
| 696 | const select = !this.selectedCharacters.includes(characterId); | 696 | const select = !this.selectedCharacters.includes(characterId); |
| 697 | const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass); | 697 | const legacyBulkEditCheckbox = character.querySelector('.' + BulkEditOverlay.legacySelectedClass); |
| @@ -699,11 +699,11 @@ class BulkEditOverlay { | |||
| 699 | if (select) { | 699 | if (select) { |
| 700 | character.classList.add(BulkEditOverlay.selectedClass); | 700 | character.classList.add(BulkEditOverlay.selectedClass); |
| 701 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; | 701 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = true; |
| 702 | this.#selectedCharacters.push(String(characterId)); | 702 | this.#selectedCharacters.push(characterId); |
| 703 | } else { | 703 | } else { |
| 704 | character.classList.remove(BulkEditOverlay.selectedClass); | 704 | character.classList.remove(BulkEditOverlay.selectedClass); |
| 705 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; | 705 | if (legacyBulkEditCheckbox) legacyBulkEditCheckbox.checked = false; |
| 706 | this.#selectedCharacters = this.#selectedCharacters.filter(item => String(characterId) !== item); | 706 | this.#selectedCharacters = this.#selectedCharacters.filter(item => characterId !== item); |
| 707 | } | 707 | } |
| 708 | 708 | ||
| 709 | this.updateSelectedCount(); | 709 | this.updateSelectedCount(); |
| @@ -732,15 +732,15 @@ class BulkEditOverlay { | |||
| 732 | * @param {boolean} select - <c>true</c> if the characters in the range are to be selected, <c>false</c> if deselected | 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 | toggleCharactersInRange = (currentCharacter, select) => { | 734 | toggleCharactersInRange = (currentCharacter, select) => { |
| 735 | const currentCharacterId = currentCharacter.getAttribute('chid'); | 735 | const currentCharacterId = Number(currentCharacter.getAttribute('data-chid')); |
| 736 | const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); | 736 | const characters = Array.from(document.querySelectorAll('#' + BulkEditOverlay.containerId + ' .' + BulkEditOverlay.characterClass)); |
| 737 | 737 | ||
| 738 | const startIndex = characters.findIndex(c => c.getAttribute('chid') === this.lastSelected.characterId); | 738 | const startIndex = characters.findIndex(c => Number(c.getAttribute('data-chid')) === Number(this.lastSelected.characterId)); |
| 739 | const endIndex = characters.findIndex(c => c.getAttribute('chid') === currentCharacterId); | 739 | const endIndex = characters.findIndex(c => Number(c.getAttribute('data-chid')) === currentCharacterId); |
| 740 | 740 | ||
| 741 | for (let i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) { | 741 | for (let i = Math.min(startIndex, endIndex); i <= Math.max(startIndex, endIndex); i++) { |
| 742 | const character = characters[i]; | 742 | const character = characters[i]; |
| 743 | const characterId = character.getAttribute('chid'); | 743 | const characterId = Number(character.getAttribute('data-chid')); |
| 744 | const isCharacterSelected = this.selectedCharacters.includes(characterId); | 744 | const isCharacterSelected = this.selectedCharacters.includes(characterId); |
| 745 | 745 | ||
| 746 | // Only toggle the character if it wasn't on the state we have are toggling towards. | 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 | if (active_character !== null && active_character !== undefined) { | 281 | if (active_character !== null && active_character !== undefined) { |
| 282 | const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); | 282 | const active_character_id = characters.findIndex(x => getTagKeyForEntity(x) === active_character); |
| 283 | if (active_character_id !== -1) { | 283 | if (active_character_id !== -1) { |
| 284 | await selectCharacterById(String(active_character_id)); | 284 | await selectCharacterById(active_character_id); |
| 285 | 285 | ||
| 286 | // Do a little tomfoolery to spoof the tag selector | 286 | // Do a little tomfoolery to spoof the tag selector |
| 287 | const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); | 287 | const selectedCharElement = $(`#rm_print_characters_block .character_select[chid="${active_character_id}"]`); |
| @@ -894,14 +894,14 @@ export function initRossMods() { | |||
| 894 | 894 | ||
| 895 | // when a char is selected from the list, save their name as the auto-load character for next page load | 895 | // when a char is selected from the list, save their name as the auto-load character for next page load |
| 896 | $(document).on('click', '.character_select', function () { | 896 | $(document).on('click', '.character_select', function () { |
| 897 | const characterId = $(this).attr('chid') || $(this).data('id'); | 897 | const characterId = $(this).attr('data-chid'); |
| 898 | setActiveCharacter(characterId); | 898 | setActiveCharacter(characterId); |
| 899 | setActiveGroup(null); | 899 | setActiveGroup(null); |
| 900 | saveSettingsDebounced(); | 900 | saveSettingsDebounced(); |
| 901 | }); | 901 | }); |
| 902 | 902 | ||
| 903 | $(document).on('click', '.group_select', function () { | 903 | $(document).on('click', '.group_select', function () { |
| 904 | const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id'); | 904 | const groupId = $(this).attr('data-chid') || $(this).attr('data-grid'); |
| 905 | setActiveCharacter(null); | 905 | setActiveCharacter(null); |
| 906 | setActiveGroup(groupId); | 906 | setActiveGroup(groupId); |
| 907 | saveSettingsDebounced(); | 907 | saveSettingsDebounced(); |
| @@ -691,7 +691,7 @@ export function getGroupBlock(group) { | |||
| 691 | 691 | ||
| 692 | const template = $('#group_list_template .group_select').clone(); | 692 | const template = $('#group_list_template .group_select').clone(); |
| 693 | template.data('id', group.id); | 693 | template.data('id', group.id); |
| 694 | template.attr('grid', group.id); | 694 | template.attr('data-grid', group.id); |
| 695 | template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`); | 695 | template.find('.ch_name').text(group.name).attr('title', `[Group] ${group.name}`); |
| 696 | template.find('.group_fav_icon').css('display', 'none'); | 696 | template.find('.group_fav_icon').css('display', 'none'); |
| 697 | template.addClass(group.fav ? 'is_fav' : ''); | 697 | template.addClass(group.fav ? 'is_fav' : ''); |
| @@ -1359,7 +1359,7 @@ function getGroupCharacterBlock(character) { | |||
| 1359 | template.data('id', character.avatar); | 1359 | template.data('id', character.avatar); |
| 1360 | template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar }); | 1360 | template.find('.avatar img').attr({ 'src': avatar, 'title': character.avatar }); |
| 1361 | template.find('.ch_name').text(character.name); | 1361 | template.find('.ch_name').text(character.name); |
| 1362 | template.attr('chid', characters.indexOf(character)); | 1362 | template.attr('data-chid', characters.indexOf(character)); |
| 1363 | template.find('.ch_fav').val(isFav); | 1363 | template.find('.ch_fav').val(isFav); |
| 1364 | template.toggleClass('is_fav', isFav); | 1364 | template.toggleClass('is_fav', isFav); |
| 1365 | 1365 | ||
| @@ -1645,7 +1645,7 @@ async function onGroupActionClick(event) { | |||
| 1645 | } | 1645 | } |
| 1646 | 1646 | ||
| 1647 | if (action === 'speak') { | 1647 | if (action === 'speak') { |
| 1648 | const chid = Number(member.attr('chid')); | 1648 | const chid = Number(member.attr('data-chid')); |
| 1649 | if (Number.isInteger(chid)) { | 1649 | if (Number.isInteger(chid)) { |
| 1650 | Generate('normal', { force_chid: chid }); | 1650 | Generate('normal', { force_chid: chid }); |
| 1651 | } | 1651 | } |
| @@ -1700,7 +1700,7 @@ function openCharacterDefinition(characterSelect) { | |||
| 1700 | return; | 1700 | return; |
| 1701 | } | 1701 | } |
| 1702 | 1702 | ||
| 1703 | const chid = characterSelect.attr('chid'); | 1703 | const chid = characterSelect.attr('data-chid'); |
| 1704 | 1704 | ||
| 1705 | if (chid === null || chid === undefined) { | 1705 | if (chid === null || chid === undefined) { |
| 1706 | return; | 1706 | return; |
| @@ -2018,7 +2018,7 @@ jQuery(() => { | |||
| 2018 | } | 2018 | } |
| 2019 | 2019 | ||
| 2020 | $(document).on('click', '.group_select', function () { | 2020 | $(document).on('click', '.group_select', function () { |
| 2021 | const groupId = $(this).attr('chid') || $(this).attr('grid') || $(this).data('id'); | 2021 | const groupId = $(this).attr('data-chid') || $(this).attr('data-grid'); |
| 2022 | openGroupById(groupId); | 2022 | openGroupById(groupId); |
| 2023 | }); | 2023 | }); |
| 2024 | $('#rm_group_filter').on('input', filterGroupMembers); | 2024 | $('#rm_group_filter').on('input', filterGroupMembers); |
| @@ -1,4 +1,6 @@ | |||
| 1 | import { | 1 | import { |
| 2 | buildAvatarList, | ||
| 3 | characterToEntity, | ||
| 2 | characters, | 4 | characters, |
| 3 | chat, | 5 | chat, |
| 4 | chat_metadata, | 6 | chat_metadata, |
| @@ -7,7 +9,10 @@ import { | |||
| 7 | event_types, | 9 | event_types, |
| 8 | getRequestHeaders, | 10 | getRequestHeaders, |
| 9 | getThumbnailUrl, | 11 | getThumbnailUrl, |
| 12 | groupToEntity, | ||
| 13 | menu_type, | ||
| 10 | name1, | 14 | name1, |
| 15 | name2, | ||
| 11 | reloadCurrentChat, | 16 | reloadCurrentChat, |
| 12 | saveChatConditional, | 17 | saveChatConditional, |
| 13 | saveMetadata, | 18 | saveMetadata, |
| @@ -17,23 +22,63 @@ import { | |||
| 17 | } from '../script.js'; | 22 | } from '../script.js'; |
| 18 | import { persona_description_positions, power_user } from './power-user.js'; | 23 | import { persona_description_positions, power_user } from './power-user.js'; |
| 19 | import { getTokenCountAsync } from './tokenizers.js'; | 24 | import { getTokenCountAsync } from './tokenizers.js'; |
| 20 | import { PAGINATION_TEMPLATE, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, parseJsonFile } from './utils.js'; | 25 | import { PAGINATION_TEMPLATE, clearInfoBlock, debounce, delay, download, ensureImageFormatSupported, flashHighlight, getBase64Async, getCharIndex, isFalseBoolean, isTrueBoolean, onlyUnique, parseJsonFile, setInfoBlock } from './utils.js'; |
| 21 | import { debounce_timeout } from './constants.js'; | 26 | import { debounce_timeout } from './constants.js'; |
| 22 | import { FILTER_TYPES, FilterHelper } from './filters.js'; | 27 | import { FILTER_TYPES, FilterHelper } from './filters.js'; |
| 23 | import { selected_group } from './group-chats.js'; | 28 | import { groups, selected_group } from './group-chats.js'; |
| 24 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; | 29 | import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| 25 | import { t } from './i18n.js'; | 30 | import { t } from './i18n.js'; |
| 26 | import { openWorldInfoEditor, world_names } from './world-info.js'; | 31 | import { openWorldInfoEditor, world_names } from './world-info.js'; |
| 27 | import { renderTemplateAsync } from './templates.js'; | 32 | import { renderTemplateAsync } from './templates.js'; |
| 33 | import { saveMetadataDebounced } from './extensions.js'; | ||
| 28 | import { accountStorage } from './util/AccountStorage.js'; | 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 | let savePersonasPage = 0; | 60 | let savePersonasPage = 0; |
| 31 | const GRID_STORAGE_KEY = 'Personas_GridView'; | 61 | const GRID_STORAGE_KEY = 'Personas_GridView'; |
| 32 | const DEFAULT_DEPTH = 2; | 62 | const DEFAULT_DEPTH = 2; |
| 33 | const DEFAULT_ROLE = 0; | 63 | const DEFAULT_ROLE = 0; |
| 64 | |||
| 65 | /** @type {string} The currently selected persona (identified by its avatar) */ | ||
| 34 | export let user_avatar = ''; | 66 | export let user_avatar = ''; |
| 67 | |||
| 68 | /** @type {FilterHelper} Filter helper for the persona list */ | ||
| 35 | export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick)); | 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 | function switchPersonaGridView() { | 82 | function switchPersonaGridView() { |
| 38 | const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true'; | 83 | const state = accountStorage.getItem(GRID_STORAGE_KEY) === 'true'; |
| 39 | $('#user_avatar_block').toggleClass('gridView', state); | 84 | $('#user_avatar_block').toggleClass('gridView', state); |
| @@ -45,33 +90,31 @@ function switchPersonaGridView() { | |||
| 45 | * @returns {string} User avatar URL | 90 | * @returns {string} User avatar URL |
| 46 | */ | 91 | */ |
| 47 | export function getUserAvatar(avatarImg) { | 92 | export function getUserAvatar(avatarImg) { |
| 48 | return `User Avatars/${avatarImg}`; | 93 | return `${USER_AVATAR_PATH}${avatarImg}`; |
| 49 | } | 94 | } |
| 50 | 95 | ||
| 51 | export function initUserAvatar(avatar) { | 96 | export function initUserAvatar(avatar) { |
| 52 | user_avatar = avatar; | 97 | user_avatar = avatar; |
| 53 | reloadUserAvatar(); | 98 | reloadUserAvatar(); |
| 54 | highlightSelectedAvatar(); | 99 | updatePersonaUIStates(); |
| 55 | } | 100 | } |
| 56 | 101 | ||
| 57 | /** | 102 | /** |
| 58 | * Sets a user avatar file | 103 | * Sets a user avatar file |
| 59 | * @param {string} imgfile Link to an image file | 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 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('imgfile'); | 110 | user_avatar = imgfile && typeof imgfile === 'string' ? imgfile : $(this).attr('data-avatar-id'); |
| 63 | reloadUserAvatar(); | 111 | reloadUserAvatar(); |
| 64 | highlightSelectedAvatar(); | 112 | updatePersonaUIStates({ navigateToCurrent: navigateToCurrent }); |
| 65 | selectCurrentPersona(); | 113 | selectCurrentPersona({ toastPersonaNameChange: toastPersonaNameChange }); |
| 66 | saveSettingsDebounced(); | 114 | saveSettingsDebounced(); |
| 67 | $('.zoomed_avatar[forchar]').remove(); | 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 | function reloadUserAvatar(force = false) { | 118 | function reloadUserAvatar(force = false) { |
| 76 | $('.mes').each(function () { | 119 | $('.mes').each(function () { |
| 77 | const avatarImg = $(this).find('.avatar img'); | 120 | const avatarImg = $(this).find('.avatar img'); |
| @@ -131,29 +174,49 @@ function verifyPersonaSearchSortRule() { | |||
| 131 | 174 | ||
| 132 | /** | 175 | /** |
| 133 | * Gets a rendered avatar block. | 176 | * Gets a rendered avatar block. |
| 134 | * @param {string} name Avatar file name | 177 | * @param {string} avatarId Avatar file name |
| 135 | * @returns {JQuery<HTMLElement>} Avatar block | 178 | * @returns {JQuery<HTMLElement>} Avatar block |
| 136 | */ | 179 | */ |
| 137 | function getUserAvatarBlock(name) { | 180 | function getUserAvatarBlock(avatarId) { |
| 138 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; | 181 | const isFirefox = navigator.userAgent.toLowerCase().indexOf('firefox') > -1; |
| 139 | const template = $('#user_avatar_template .avatar-container').clone(); | 182 | const template = $('#user_avatar_template .avatar-container').clone(); |
| 140 | const personaName = power_user.personas[name]; | 183 | const personaName = power_user.personas[avatarId]; |
| 141 | const personaDescription = power_user.persona_descriptions[name]?.description; | 184 | const personaDescription = power_user.persona_descriptions[avatarId]?.description; |
| 185 | |||
| 142 | template.find('.ch_name').text(personaName || '[Unnamed Persona]'); | 186 | template.find('.ch_name').text(personaName || '[Unnamed Persona]'); |
| 143 | template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); | 187 | template.find('.ch_description').text(personaDescription || $('#user_avatar_block').attr('no_desc_text')).toggleClass('text_muted', !personaDescription); |
| 144 | template.attr('imgfile', name); | 188 | template.attr('data-avatar-id', avatarId); |
| 145 | template.find('.avatar').attr('imgfile', name).attr('title', name); | 189 | template.find('.avatar').attr('data-avatar-id', avatarId).attr('title', avatarId); |
| 146 | template.toggleClass('default_persona', name === power_user.default_persona); | 190 | template.toggleClass('default_persona', avatarId === power_user.default_persona); |
| 147 | let avatarUrl = getUserAvatar(name); | 191 | let avatarUrl = getUserAvatar(avatarId); |
| 148 | if (isFirefox) { | 192 | if (isFirefox) { |
| 149 | avatarUrl += '?t=' + Date.now(); | 193 | avatarUrl += '?t=' + Date.now(); |
| 150 | } | 194 | } |
| 151 | template.find('img').attr('src', avatarUrl); | 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 | $('#user_avatar_block').append(template); | 203 | $('#user_avatar_block').append(template); |
| 153 | return template; | 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 | * Gets a list of user avatars. | 220 | * Gets a list of user avatars. |
| 158 | * @param {boolean} doRender Whether to render the list | 221 | * @param {boolean} doRender Whether to render the list |
| 159 | * @param {string} openPageAt Item to be opened at | 222 | * @param {string} openPageAt Item to be opened at |
| @@ -175,6 +238,8 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | |||
| 175 | return allEntities; | 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 | // Before printing the personas, we check if we should enable/disable search sorting | 243 | // Before printing the personas, we check if we should enable/disable search sorting |
| 179 | verifyPersonaSearchSortRule(); | 244 | verifyPersonaSearchSortRule(); |
| 180 | 245 | ||
| @@ -203,7 +268,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | |||
| 203 | for (const item of data) { | 268 | for (const item of data) { |
| 204 | $(listId).append(getUserAvatarBlock(item)); | 269 | $(listId).append(getUserAvatarBlock(item)); |
| 205 | } | 270 | } |
| 206 | highlightSelectedAvatar(); | 271 | updatePersonaUIStates(); |
| 207 | }, | 272 | }, |
| 208 | afterSizeSelectorChange: function (e) { | 273 | afterSizeSelectorChange: function (e) { |
| 209 | accountStorage.setItem(storageKey, e.target.value); | 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 | if (openPageAt) { | 284 | navigateToAvatar = (avatarId) => { |
| 220 | const avatarIndex = entities.indexOf(openPageAt); | 285 | const avatarIndex = entities.indexOf(avatarId); |
| 221 | const page = Math.floor(avatarIndex / perPage) + 1; | 286 | const page = Math.floor(avatarIndex / perPage) + 1; |
| 222 | 287 | ||
| 223 | if (avatarIndex !== -1) { | 288 | if (avatarIndex !== -1) { |
| 224 | $('#persona_pagination_container').pagination('go', page); | 289 | $('#persona_pagination_container').pagination('go', page); |
| 225 | } | 290 | } |
| 226 | } | 291 | }; |
| 292 | |||
| 293 | openPageAt && navigateToAvatar(openPageAt); | ||
| 227 | 294 | ||
| 228 | return allEntities; | 295 | return allEntities; |
| 229 | } | 296 | } |
| @@ -233,7 +300,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') { | |||
| 233 | * Uploads an avatar file to the server | 300 | * Uploads an avatar file to the server |
| 234 | * @param {string} url URL for the avatar file | 301 | * @param {string} url URL for the avatar file |
| 235 | * @param {string} [name] Optional name for the avatar file | 302 | * @param {string} [name] Optional name for the avatar file |
| 236 | * @returns {Promise} Promise object representing the AJAX request | 303 | * @returns {Promise} Promise that resolves when the avatar is uploaded |
| 237 | */ | 304 | */ |
| 238 | async function uploadUserAvatar(url, name) { | 305 | async function uploadUserAvatar(url, name) { |
| 239 | const fetchResult = await fetch(url); | 306 | const fetchResult = await fetch(url); |
| @@ -246,18 +313,17 @@ async function uploadUserAvatar(url, name) { | |||
| 246 | formData.append('overwrite_name', name); | 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 | cache: false, | 321 | headers: headers, |
| 255 | contentType: false, | 322 | cache: 'no-cache', |
| 256 | processData: false, | 323 | body: formData, |
| 257 | success: async function () { | ||
| 258 | await getUserAvatars(true, name); | ||
| 259 | }, | ||
| 260 | }); | 324 | }); |
| 325 | |||
| 326 | await getUserAvatars(true, name); | ||
| 261 | } | 327 | } |
| 262 | 328 | ||
| 263 | async function changeUserAvatar(e) { | 329 | async function changeUserAvatar(e) { |
| @@ -298,15 +364,19 @@ async function changeUserAvatar(e) { | |||
| 298 | formData.set('avatar', convertedFile); | 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, | 369 | |
| 304 | data: formData, | 370 | const response = await fetch(url, { |
| 305 | beforeSend: () => { }, | 371 | method: 'POST', |
| 306 | cache: false, | 372 | headers: headers, |
| 307 | contentType: false, | 373 | cache: 'no-cache', |
| 308 | processData: false, | 374 | body: formData, |
| 309 | success: async function (data) { | 375 | }); |
| 376 | |||
| 377 | if (response.ok) { | ||
| 378 | const data = await response.json(); | ||
| 379 | |||
| 310 | // If the user uploaded a new avatar, we want to make sure it's not cached | 380 | // If the user uploaded a new avatar, we want to make sure it's not cached |
| 311 | const name = formData.get('overwrite_name'); | 381 | const name = formData.get('overwrite_name'); |
| 312 | if (name) { | 382 | if (name) { |
| @@ -321,9 +391,7 @@ async function changeUserAvatar(e) { | |||
| 321 | } | 391 | } |
| 322 | 392 | ||
| 323 | await getUserAvatars(true, name || data.path); | 393 | await getUserAvatars(true, name || data.path); |
| 324 | }, | 394 | } |
| 325 | error: (jqXHR, exception) => { }, | ||
| 326 | }); | ||
| 327 | 395 | ||
| 328 | // Will allow to select the same file twice in a row | 396 | // Will allow to select the same file twice in a row |
| 329 | form.reset(); | 397 | form.reset(); |
| @@ -384,13 +452,25 @@ export function initPersona(avatarId, personaName, personaDescription) { | |||
| 384 | saveSettingsDebounced(); | 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 | export async function convertCharacterToPersona(characterId = null) { | 467 | export async function convertCharacterToPersona(characterId = null) { |
| 388 | if (null === characterId) characterId = this_chid; | 468 | if (null === characterId) characterId = this_chid; |
| 389 | 469 | ||
| 390 | const avatarUrl = characters[characterId]?.avatar; | 470 | const avatarUrl = characters[characterId]?.avatar; |
| 391 | if (!avatarUrl) { | 471 | if (!avatarUrl) { |
| 392 | console.log('No avatar found for this character'); | 472 | console.log('No avatar found for this character'); |
| 393 | return; | 473 | return false; |
| 394 | } | 474 | } |
| 395 | 475 | ||
| 396 | const name = characters[characterId]?.name; | 476 | const name = characters[characterId]?.name; |
| @@ -401,7 +481,7 @@ export async function convertCharacterToPersona(characterId = null) { | |||
| 401 | const confirm = await Popup.show.confirm(t`Overwrite Existing Persona`, t`This character exists as a persona already. Do you want to overwrite it?`); | 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 | if (!confirm) { | 482 | if (!confirm) { |
| 403 | console.log('User cancelled the overwrite of the persona'); | 483 | console.log('User cancelled the overwrite of the persona'); |
| 404 | return; | 484 | return false; |
| 405 | } | 485 | } |
| 406 | } | 486 | } |
| 407 | 487 | ||
| @@ -439,6 +519,7 @@ export async function convertCharacterToPersona(characterId = null) { | |||
| 439 | await getUserAvatars(true, overwriteName); | 519 | await getUserAvatars(true, overwriteName); |
| 440 | // Reload the persona description | 520 | // Reload the persona description |
| 441 | setPersonaDescription(); | 521 | setPersonaDescription(); |
| 522 | return true; | ||
| 442 | } | 523 | } |
| 443 | 524 | ||
| 444 | /** | 525 | /** |
| @@ -450,7 +531,12 @@ const countPersonaDescriptionTokens = debounce(async () => { | |||
| 450 | $('#persona_description_token_count').text(String(count)); | 531 | $('#persona_description_token_count').text(String(count)); |
| 451 | }, debounce_timeout.relaxed); | 532 | }, debounce_timeout.relaxed); |
| 452 | 533 | ||
| 534 | /** | ||
| 535 | * Updates the UI for the Persona Management page with the current persona values | ||
| 536 | */ | ||
| 453 | export function setPersonaDescription() { | 537 | export function setPersonaDescription() { |
| 538 | $('#your_name').text(name1); | ||
| 539 | |||
| 454 | if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) { | 540 | if (power_user.persona_description_position === persona_description_positions.AFTER_CHAR) { |
| 455 | power_user.persona_description_position = persona_description_positions.IN_PROMPT; | 541 | power_user.persona_description_position = persona_description_positions.IN_PROMPT; |
| 456 | } | 542 | } |
| @@ -468,116 +554,219 @@ export function setPersonaDescription() { | |||
| 468 | .prop('selected', String(true)); | 554 | .prop('selected', String(true)); |
| 469 | $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); | 555 | $('#persona_lore_button').toggleClass('world_set', !!power_user.persona_description_lorebook); |
| 470 | countPersonaDescriptionTokens(); | 556 | countPersonaDescriptionTokens(); |
| 471 | } | ||
| 472 | 557 | ||
| 473 | export function autoSelectPersona(name) { | 558 | updatePersonaUIStates(); |
| 474 | for (const [key, value] of Object.entries(power_user.personas)) { | 559 | updatePersonaConnectionsAvatarList(); |
| 475 | if (value === name) { | ||
| 476 | console.log(`Auto-selecting persona ${key} for name ${name}`); | ||
| 477 | setUserAvatar(key); | ||
| 478 | return; | ||
| 479 | } | 560 | } |
| 561 | |||
| 562 | /** | ||
| 563 | * Gets a list of all personas in the current chat. | ||
| 564 | * | ||
| 565 | * @returns {string[]} An array of persona identifiers | ||
| 566 | */ | ||
| 567 | function getPersonasOfCurrentChat() { | ||
| 568 | const personas = chat.filter(message => String(message.force_avatar).startsWith(USER_AVATAR_PATH)) | ||
| 569 | .map(message => message.force_avatar.replace(USER_AVATAR_PATH, '')) | ||
| 570 | .filter(onlyUnique); | ||
| 571 | return personas; | ||
| 480 | } | 572 | } |
| 573 | |||
| 574 | /** | ||
| 575 | * Builds a list of persona avatars and populates the given block element with them. | ||
| 576 | * | ||
| 577 | * @param {HTMLElement} block - The HTML element where the avatar 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 | ||
| 583 | */ | ||
| 584 | export function buildPersonaAvatarList(block, personas, { empty = true, interactable = false, highlightFavs = true } = {}) { | ||
| 585 | const personaEntities = personas.map(avatar => ({ | ||
| 586 | type: 'persona', | ||
| 587 | id: avatar, | ||
| 588 | item: { | ||
| 589 | name: power_user.personas[avatar], | ||
| 590 | description: power_user.persona_descriptions[avatar]?.description || '', | ||
| 591 | avatar: avatar, | ||
| 592 | fav: power_user.default_persona === avatar, | ||
| 593 | }, | ||
| 594 | })); | ||
| 595 | |||
| 596 | buildAvatarList($(block), personaEntities, { empty: empty, interactable: interactable, highlightFavs: highlightFavs }); | ||
| 481 | } | 597 | } |
| 482 | 598 | ||
| 483 | /** | 599 | /** |
| 484 | * Updates the name of a persona if it exists. | 600 | * Displays avatar connections for the current persona. |
| 485 | * @param {string} avatarId User avatar id | 601 | * Converts connections to entities and populates the avatar list. Shows a message if no connections are found. |
| 486 | * @param {string} newName New name for the persona | ||
| 487 | */ | 602 | */ |
| 488 | async function updatePersonaNameIfExists(avatarId, newName) { | 603 | export function updatePersonaConnectionsAvatarList() { |
| 489 | if (avatarId in power_user.personas) { | 604 | /** @type {PersonaConnection[]} */ |
| 490 | power_user.personas[avatarId] = newName; | 605 | const connections = power_user.persona_descriptions[user_avatar]?.connections ?? []; |
| 491 | console.log(`Updated persona name for ${avatarId} to ${newName}`); | 606 | const entities = connections.map(connection => { |
| 492 | } else { | 607 | if (connection.type === 'character') { |
| 493 | power_user.personas[avatarId] = newName; | 608 | const character = characters.find(c => c.avatar === connection.id); |
| 494 | power_user.persona_descriptions[avatarId] = { | 609 | if (character) return characterToEntity(character, getCharIndex(character)); |
| 495 | description: '', | ||
| 496 | position: persona_description_positions.IN_PROMPT, | ||
| 497 | depth: DEFAULT_DEPTH, | ||
| 498 | role: DEFAULT_ROLE, | ||
| 499 | lorebook: '', | ||
| 500 | }; | ||
| 501 | console.log(`Created persona name for ${avatarId} as ${newName}`); | ||
| 502 | } | 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); | ||
| 503 | 617 | ||
| 504 | await getUserAvatars(true, avatarId); | 618 | if (entities.length) |
| 505 | saveSettingsDebounced(); | 619 | buildAvatarList($('#persona_connections_list'), entities, { interactable: true }); |
| 620 | else | ||
| 621 | $('#persona_connections_list').text(t`[No character connections. Click one of the buttons above to connect this persona.]`); | ||
| 506 | } | 622 | } |
| 507 | 623 | ||
| 508 | async function bindUserNameToPersona(e) { | ||
| 509 | e?.stopPropagation(); | ||
| 510 | const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | ||
| 511 | 624 | ||
| 512 | if (!avatarId) { | 625 | /** |
| 513 | console.warn('No avatar id found'); | 626 | * Displays a popup for persona selection and returns the selected persona. |
| 514 | return; | 627 | * |
| 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 | }); | ||
| 515 | } | 671 | } |
| 516 | 672 | ||
| 517 | let personaUnbind = false; | 673 | if (personasToHighlight && personasToHighlight.includes(block.dataset.pid)) { |
| 518 | const existingPersona = power_user.personas[avatarId]; | 674 | block.classList.add('is_active'); |
| 519 | const personaName = await Popup.show.input( | 675 | block.title = block.title + '\n\n' + t`Was used in current chat.`; |
| 520 | t`Enter a name for this persona:`, | 676 | if (block.classList.contains('is_fav')) block.title = block.title + '\n' + t`Is your default persona.`; |
| 521 | t`(If empty name is provided, this will unbind the name from this avatar)`, | 677 | } |
| 522 | existingPersona || '', | 678 | }); |
| 523 | { onClose: (p) => { personaUnbind = p.value === '' && p.result === POPUP_RESULT.AFFIRMATIVE; } }); | ||
| 524 | 679 | ||
| 525 | // If the user clicked cancel, don't do anything | 680 | /** @type {import('./popup.js').CustomPopupButton[]} */ |
| 526 | if (personaName === null && !personaUnbind) { | 681 | const customButtons = []; |
| 527 | return; | 682 | if (targetedChar) { |
| 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 | } | ||
| 528 | } | 696 | } |
| 529 | 697 | ||
| 530 | if (personaName && personaName.length > 0) { | 698 | saveSettingsDebounced(); |
| 531 | // If the user clicked ok and entered a name, bind the name to the persona | 699 | updatePersonaConnectionsAvatarList(); |
| 532 | console.log(`Binding persona ${avatarId} to name ${personaName}`); | 700 | if (power_user.persona_show_notifications) { |
| 533 | power_user.personas[avatarId] = personaName; | 701 | const name = targetedChar.type == 'character' ? characters[targetedChar.id]?.name : groups[targetedChar.id]?.name; |
| 534 | const descriptor = power_user.persona_descriptions[avatarId]; | 702 | toastr.info(t`All connections to ${name} have been removed.`, t`Personas Unlocked`); |
| 535 | const isCurrentPersona = avatarId === user_avatar; | 703 | } |
| 704 | }, | ||
| 705 | }); | ||
| 706 | } | ||
| 536 | 707 | ||
| 537 | // Create a description object if it doesn't exist | 708 | const popup = new Popup(content, POPUP_TYPE.TEXT, '', { okButton: okButton, customButtons: customButtons }); |
| 538 | if (!descriptor) { | 709 | const result = await popup.show(); |
| 539 | // If the user is currently using this persona, set the description to the current description | 710 | return Number(result) >= 100 ? personas[Number(result) - 100] : null; |
| 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 | } | 711 | } |
| 548 | 712 | ||
| 549 | // If the user is currently using this persona, update the name | 713 | /** |
| 550 | if (isCurrentPersona) { | 714 | * Automatically selects a persona based on the given name if a matching persona exists. |
| 551 | console.log(`Auto-updating user name to ${personaName}`); | 715 | * @param {string} name - The name to search for |
| 552 | setUserName(personaName); | 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 | } else { | 725 | } |
| 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 | saveSettingsDebounced(); | 749 | saveSettingsDebounced(); |
| 562 | await getUserAvatars(true, avatarId); | 750 | await getUserAvatars(true, avatarId); |
| 751 | updatePersonaUIStates(); | ||
| 563 | setPersonaDescription(); | 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 | const personaName = power_user.personas[user_avatar]; | 763 | const personaName = power_user.personas[user_avatar]; |
| 568 | if (personaName) { | 764 | if (personaName) { |
| 569 | const lockedPersona = chat_metadata['persona']; | 765 | const shouldAutoLock = 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 | if (personaName !== name1) { | 767 | if (personaName !== name1) { |
| 579 | console.log(`Auto-updating user name to ${personaName}`); | 768 | console.log(`Auto-updating user name to ${personaName}`); |
| 580 | setUserName(personaName); | 769 | setUserName(personaName, { toastPersonaNameChange: !shouldAutoLock && toastPersonaNameChange }); |
| 581 | } | 770 | } |
| 582 | 771 | ||
| 583 | const descriptor = power_user.persona_descriptions[user_avatar]; | 772 | const descriptor = power_user.persona_descriptions[user_avatar]; |
| @@ -600,67 +789,144 @@ function selectCurrentPersona() { | |||
| 600 | depth: DEFAULT_DEPTH, | 789 | depth: DEFAULT_DEPTH, |
| 601 | role: DEFAULT_ROLE, | 790 | role: DEFAULT_ROLE, |
| 602 | lorebook: '', | 791 | lorebook: '', |
| 792 | connections: [], | ||
| 603 | }; | 793 | }; |
| 604 | } | 794 | } |
| 605 | 795 | ||
| 606 | setPersonaDescription(); | 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`); | ||
| 607 | } | 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 | } | ||
| 820 | } | ||
| 821 | } | ||
| 822 | |||
| 823 | /** | ||
| 824 | * Checks if a connection is locked for the current 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); | ||
| 608 | } | 831 | } |
| 609 | 832 | ||
| 610 | /** | 833 | /** |
| 611 | * Checks if the persona is locked for the current chat. | 834 | * Checks if the persona is locked |
| 835 | * @param {PersonaLockType} type - Lock type | ||
| 612 | * @returns {boolean} Whether the persona is locked | 836 | * @returns {boolean} Whether the persona is locked |
| 613 | */ | 837 | */ |
| 614 | function isPersonaLocked() { | 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 | * Locks or unlocks the persona for the current chat. | 852 | * Locks or unlocks the persona |
| 620 | * @param {boolean} state Desired lock state | 853 | * @param {boolean} state Desired lock state |
| 854 | * @param {PersonaLockType} type - Lock type | ||
| 621 | * @returns {Promise<void>} | 855 | * @returns {Promise<void>} |
| 622 | */ | 856 | */ |
| 623 | export async function setPersonaLockState(state) { | 857 | export async function setPersonaLockState(state, type = 'chat') { |
| 624 | return state ? await lockPersona() : await unlockPersona(); | 858 | return state ? await lockPersona(type) : await unlockPersona(type); |
| 625 | } | 859 | } |
| 626 | 860 | ||
| 627 | /** | 861 | /** |
| 628 | * Toggle the persona lock state for the current chat. | 862 | * Toggle the persona lock state |
| 629 | * @returns {Promise<void>} | 863 | * @param {PersonaLockType} type - Lock type |
| 864 | * @returns {Promise<boolean>} - Whether the persona was locked | ||
| 630 | */ | 865 | */ |
| 631 | export async function togglePersonaLock() { | 866 | export async function togglePersonaLock(type = 'chat') { |
| 632 | return isPersonaLocked() | 867 | if (isPersonaLocked(type)) { |
| 633 | ? await unlockPersona() | 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 | * Unlock the persona for the current chat. | 877 | * Unlock the persona |
| 878 | * @param {PersonaLockType} type - Lock type | ||
| 879 | * @returns {Promise<void>} | ||
| 639 | */ | 880 | */ |
| 640 | async function unlockPersona() { | 881 | async function unlockPersona(type = 'chat') { |
| 882 | switch (type) { | ||
| 883 | case 'default': { | ||
| 884 | // TODO: Make this toggle-able | ||
| 885 | await toggleDefaultPersona(user_avatar, { quiet: true }); | ||
| 886 | break; | ||
| 887 | } | ||
| 888 | case 'chat': { | ||
| 641 | if (chat_metadata['persona']) { | 889 | if (chat_metadata['persona']) { |
| 642 | console.log(`Unlocking persona for this chat ${chat_metadata['persona']}`); | 890 | console.log(`Unlocking persona ${user_avatar} from this chat`); |
| 643 | delete chat_metadata['persona']; | 891 | delete chat_metadata['persona']; |
| 644 | await saveMetadata(); | 892 | await saveMetadata(); |
| 645 | if (power_user.persona_show_notifications) { | 893 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 646 | toastr.info(t`User persona is now unlocked for this chat. Click the "Lock" again to revert.`, t`Persona unlocked`); | 894 | toastr.info(t`Persona ${name1} is now unlocked from this chat.`, t`Persona Unlocked`); |
| 895 | } | ||
| 896 | } | ||
| 897 | break; | ||
| 898 | } | ||
| 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`); | ||
| 647 | } | 909 | } |
| 648 | updateUserLockIcon(); | ||
| 649 | } | 910 | } |
| 911 | break; | ||
| 912 | } | ||
| 913 | default: | ||
| 914 | throw new Error(`Unknown persona lock type: ${type}`); | ||
| 915 | } | ||
| 916 | |||
| 917 | updatePersonaUIStates(); | ||
| 650 | } | 918 | } |
| 651 | 919 | ||
| 652 | /** | 920 | /** |
| 653 | * Lock the persona for the current chat. | 921 | * Lock the persona |
| 922 | * @param {PersonaLockType} type - Lock type | ||
| 654 | */ | 923 | */ |
| 655 | async function lockPersona() { | 924 | async function lockPersona(type = 'chat') { |
| 925 | // First make sure that user_avatar is actually a persona | ||
| 656 | if (!(user_avatar in power_user.personas)) { | 926 | if (!(user_avatar in power_user.personas)) { |
| 657 | console.log(`Creating a new persona ${user_avatar}`); | 927 | console.log(`Creating a new persona ${user_avatar}`); |
| 658 | if (power_user.persona_show_notifications) { | 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 | power_user.personas[user_avatar] = name1; | 931 | power_user.personas[user_avatar] = name1; |
| 666 | power_user.persona_descriptions[user_avatar] = { | 932 | power_user.persona_descriptions[user_avatar] = { |
| @@ -669,36 +935,75 @@ async function lockPersona() { | |||
| 669 | depth: DEFAULT_DEPTH, | 935 | depth: DEFAULT_DEPTH, |
| 670 | role: DEFAULT_ROLE, | 936 | role: DEFAULT_ROLE, |
| 671 | lorebook: '', | 937 | lorebook: '', |
| 938 | connections: [], | ||
| 672 | }; | 939 | }; |
| 673 | } | 940 | } |
| 674 | 941 | ||
| 942 | switch (type) { | ||
| 943 | case 'default': { | ||
| 944 | await toggleDefaultPersona(user_avatar, { quiet: true }); | ||
| 945 | break; | ||
| 946 | } | ||
| 947 | case 'chat': { | ||
| 948 | console.log(`Locking persona ${user_avatar} to this chat`); | ||
| 675 | chat_metadata['persona'] = user_avatar; | 949 | chat_metadata['persona'] = user_avatar; |
| 676 | await saveMetadata(); | 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 | |||
| 677 | saveSettingsDebounced(); | 977 | saveSettingsDebounced(); |
| 678 | console.log(`Locking persona for this chat ${user_avatar}`); | 978 | updatePersonaConnectionsAvatarList(); |
| 679 | if (power_user.persona_show_notifications) { | 979 | if (power_user.persona_show_notifications) { |
| 680 | toastr.success(t`User persona is locked to ${name1} in this chat`); | 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 | } | ||
| 681 | } | 987 | } |
| 682 | updateUserLockIcon(); | 988 | break; |
| 989 | } | ||
| 990 | default: | ||
| 991 | throw new Error(`Unknown persona lock type: ${type}`); | ||
| 992 | } | ||
| 993 | |||
| 994 | updatePersonaUIStates(); | ||
| 683 | } | 995 | } |
| 684 | 996 | ||
| 685 | 997 | ||
| 686 | async function deleteUserAvatar(e) { | 998 | async function deleteUserAvatar() { |
| 687 | e?.stopPropagation(); | 999 | const avatarId = user_avatar; |
| 688 | const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | ||
| 689 | 1000 | ||
| 690 | if (!avatarId) { | 1001 | if (!avatarId) { |
| 691 | console.warn('No avatar id found'); | 1002 | console.warn('No avatar id found'); |
| 692 | return; | 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 | if (!confirm) { | 1008 | if (!confirm) { |
| 704 | console.debug('User cancelled deleting avatar'); | 1009 | console.debug('User cancelled deleting avatar'); |
| @@ -719,19 +1024,20 @@ async function deleteUserAvatar(e) { | |||
| 719 | delete power_user.persona_descriptions[avatarId]; | 1024 | delete power_user.persona_descriptions[avatarId]; |
| 720 | 1025 | ||
| 721 | if (avatarId === power_user.default_persona) { | 1026 | if (avatarId === power_user.default_persona) { |
| 722 | toastr.warning(t`The default persona was deleted. You will need to set a new default persona.`, t`Default persona deleted`); | 1027 | toastr.warning(t`The default persona was deleted. You will need to set a new default persona.`, t`Default Persona Deleted`); |
| 723 | power_user.default_persona = null; | 1028 | power_user.default_persona = null; |
| 724 | } | 1029 | } |
| 725 | 1030 | ||
| 726 | if (avatarId === chat_metadata['persona']) { | 1031 | if (avatarId === chat_metadata['persona']) { |
| 727 | toastr.warning(t`The locked persona was deleted. You will need to set a new persona for this chat.`, t`Persona deleted`); | 1032 | toastr.warning(t`The locked persona was deleted. You will need to set a new persona for this chat.`, t`Persona Deleted`); |
| 728 | delete chat_metadata['persona']; | 1033 | delete chat_metadata['persona']; |
| 729 | await saveMetadata(); | 1034 | await saveMetadata(); |
| 730 | } | 1035 | } |
| 731 | 1036 | ||
| 732 | saveSettingsDebounced(); | 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 | * Opens a popup to set the lorebook for the current persona. | 1094 | * Opens a popup to set the lorebook for the current persona. |
| 789 | * @param {PointerEvent} event Click event | 1095 | * @param {JQuery.ClickEvent} event Click event |
| 790 | */ | 1096 | */ |
| 791 | async function onPersonaLoreButtonClick(event) { | 1097 | async function onPersonaLoreButtonClick(event) { |
| 792 | const personaName = power_user.personas[user_avatar]; | 1098 | const personaName = power_user.personas[user_avatar]; |
| 793 | const selectedLorebook = power_user.persona_description_lorebook; | 1099 | const selectedLorebook = power_user.persona_description_lorebook; |
| 794 | 1100 | ||
| 795 | if (!personaName) { | 1101 | if (!personaName) { |
| 796 | toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona name not set`); | 1102 | toastr.warning(t`You must bind a name to this persona before you can set a lorebook.`, t`Persona Name Not Set`); |
| 797 | return; | 1103 | return; |
| 798 | } | 1104 | } |
| 799 | 1105 | ||
| @@ -854,16 +1160,21 @@ function getOrCreatePersonaDescriptor() { | |||
| 854 | depth: power_user.persona_description_depth, | 1160 | depth: power_user.persona_description_depth, |
| 855 | role: power_user.persona_description_role, | 1161 | role: power_user.persona_description_role, |
| 856 | lorebook: power_user.persona_description_lorebook, | 1162 | lorebook: power_user.persona_description_lorebook, |
| 1163 | connections: [], | ||
| 857 | }; | 1164 | }; |
| 858 | power_user.persona_descriptions[user_avatar] = object; | 1165 | power_user.persona_descriptions[user_avatar] = object; |
| 859 | } | 1166 | } |
| 860 | return object; | 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 | if (!avatarId) { | 1178 | if (!avatarId) { |
| 868 | console.warn('No avatar id found'); | 1179 | console.warn('No avatar id found'); |
| 869 | return; | 1180 | return; |
| @@ -873,78 +1184,264 @@ async function setDefaultPersona(e) { | |||
| 873 | 1184 | ||
| 874 | if (power_user.personas[avatarId] === undefined) { | 1185 | if (power_user.personas[avatarId] === undefined) { |
| 875 | console.warn(`No persona name found for avatar ${avatarId}`); | 1186 | console.warn(`No persona name found for avatar ${avatarId}`); |
| 876 | toastr.warning(t`You must bind a name to this persona before you can set it as the default.`, t`Persona name not set`); | 1187 | toastr.warning(t`You must bind a name to this persona before you can set it as the default.`, t`Persona Name Not Set`); |
| 877 | return; | 1188 | return; |
| 878 | } | 1189 | } |
| 879 | 1190 | ||
| 880 | const personaName = power_user.personas[avatarId]; | ||
| 881 | 1191 | ||
| 882 | if (avatarId === currentDefault) { | 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 | if (!confirm) { | 1195 | if (!confirm) { |
| 886 | console.debug('User cancelled removing default persona'); | 1196 | console.debug('User cancelled removing default persona'); |
| 887 | return; | 1197 | return; |
| 888 | } | 1198 | } |
| 1199 | } | ||
| 889 | 1200 | ||
| 890 | console.log(`Removing default persona ${avatarId}`); | 1201 | console.log(`Removing default persona ${avatarId}`); |
| 891 | if (power_user.persona_show_notifications) { | 1202 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 892 | toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default persona removed`); | 1203 | toastr.info(t`This persona will no longer be used by default when you open a new chat.`, t`Default Persona Removed`); |
| 893 | } | 1204 | } |
| 894 | delete power_user.default_persona; | 1205 | delete power_user.default_persona; |
| 895 | } else { | 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`, | |
| 1209 | t`Are you sure you want to set \"${power_user.personas[avatarId]}\" as the default persona?` | ||
| 1210 | + '<br /><br />' | ||
| 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.`); | ||
| 898 | if (!confirm) { | 1212 | if (!confirm) { |
| 899 | console.debug('User cancelled setting default persona'); | 1213 | console.debug('User cancelled setting default persona'); |
| 900 | return; | 1214 | return; |
| 901 | } | 1215 | } |
| 1216 | } | ||
| 902 | 1217 | ||
| 903 | power_user.default_persona = avatarId; | 1218 | power_user.default_persona = avatarId; |
| 904 | if (power_user.persona_show_notifications) { | 1219 | if (power_user.persona_show_notifications && !isPersonaPanelOpen()) { |
| 905 | toastr.success(t`This persona will be used by default when you open a new chat.`, t`Default persona set to ${personaName}`); | 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`); |
| 906 | } | 1221 | } |
| 907 | } | 1222 | } |
| 908 | 1223 | ||
| 909 | saveSettingsDebounced(); | 1224 | saveSettingsDebounced(); |
| 910 | await getUserAvatars(true, avatarId); | 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 | // Define a persona for this chat | 1365 | // Define a persona for this chat |
| 921 | let chatPersona = ''; | 1366 | let chatPersona = ''; |
| 922 | 1367 | ||
| 923 | if (chat_metadata['persona']) { | 1368 | /** @type {'chat' | 'character' | 'default' | null} */ |
| 1369 | let connectType = null; | ||
| 1370 | |||
| 924 | // If persona is locked in chat metadata, select it | 1371 | // If persona is locked in chat metadata, select it |
| 1372 | if (chat_metadata['persona']) { | ||
| 925 | console.log(`Using locked persona ${chat_metadata['persona']}`); | 1373 | console.log(`Using locked persona ${chat_metadata['persona']}`); |
| 926 | chatPersona = chat_metadata['persona']; | 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 | if (!chatPersona) { | 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; | ||
| 941 | 1437 | ||
| 942 | // Avatar missing (persona deleted) | 1438 | if (chatPersona) connectType = 'default'; |
| 943 | if (chat_metadata['persona'] && !userAvatars.includes(chatPersona)) { | 1439 | } |
| 1440 | |||
| 1441 | // Whatever way we selected a persona, if it doesn't exist, unlock this chat | ||
| 1442 | if (chat_metadata['persona'] && !userAvatars.includes(chat_metadata['persona'])) { | ||
| 944 | console.warn('Persona avatar not found, unlocking persona'); | 1443 | console.warn('Persona avatar not found, unlocking persona'); |
| 945 | delete chat_metadata['persona']; | 1444 | delete chat_metadata['persona']; |
| 946 | updateUserLockIcon(); | ||
| 947 | return; | ||
| 948 | } | 1445 | } |
| 949 | 1446 | ||
| 950 | // Default persona missing | 1447 | // Default persona missing |
| @@ -952,12 +1449,101 @@ async function setChatLockedPersona() { | |||
| 952 | console.warn('Default persona avatar not found, clearing default persona'); | 1449 | console.warn('Default persona avatar not found, clearing default persona'); |
| 953 | power_user.default_persona = null; | 1450 | power_user.default_persona = null; |
| 954 | saveSettingsDebounced(); | 1451 | saveSettingsDebounced(); |
| 955 | return; | ||
| 956 | } | 1452 | } |
| 957 | 1453 | ||
| 958 | // Persona avatar found, select it | 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 | function onBackupPersonas() { | 1549 | function onBackupPersonas() { |
| @@ -1038,10 +1624,10 @@ async function onPersonasRestoreInput(e) { | |||
| 1038 | } | 1624 | } |
| 1039 | 1625 | ||
| 1040 | if (warnings.length) { | 1626 | if (warnings.length) { |
| 1041 | toastr.success(t`Personas restored with warnings. Check console for details.`); | 1627 | toastr.success(t`Personas restored with warnings. Check console for details.`, t`Persona Management`); |
| 1042 | console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`); | 1628 | console.warn(`PERSONA RESTORE REPORT\n====================\n${warnings.join('\n')}`); |
| 1043 | } else { | 1629 | } else { |
| 1044 | toastr.success(t`Personas restored successfully.`); | 1630 | toastr.success(t`Personas restored successfully.`, t`Persona Management`); |
| 1045 | } | 1631 | } |
| 1046 | 1632 | ||
| 1047 | await getUserAvatars(); | 1633 | await getUserAvatars(); |
| @@ -1068,6 +1654,11 @@ async function syncUserNameToPersona() { | |||
| 1068 | await reloadCurrentChat(); | 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 | export function retriggerFirstMessageOnEmptyChat() { | 1662 | export function retriggerFirstMessageOnEmptyChat() { |
| 1072 | if (this_chid >= 0 && !selected_group && chat.length === 1) { | 1663 | if (this_chid >= 0 && !selected_group && chat.length === 1) { |
| 1073 | $('#firstmessage_textarea').trigger('input'); | 1664 | $('#firstmessage_textarea').trigger('input'); |
| @@ -1083,7 +1674,7 @@ async function duplicatePersona(avatarId) { | |||
| 1083 | const personaName = power_user.personas[avatarId]; | 1674 | const personaName = power_user.personas[avatarId]; |
| 1084 | 1675 | ||
| 1085 | if (!personaName) { | 1676 | if (!personaName) { |
| 1086 | toastr.warning('Chosen avatar is not a persona'); | 1677 | toastr.warning('Chosen avatar is not a persona', t`Persona Management`); |
| 1087 | return; | 1678 | return; |
| 1088 | } | 1679 | } |
| 1089 | 1680 | ||
| @@ -1111,11 +1702,207 @@ async function duplicatePersona(avatarId) { | |||
| 1111 | saveSettingsDebounced(); | 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 | $('#create_dummy_persona').on('click', createDummyPersona); | 1906 | $('#create_dummy_persona').on('click', createDummyPersona); |
| 1120 | $('#persona_description').on('input', onPersonaDescriptionInput); | 1907 | $('#persona_description').on('input', onPersonaDescriptionInput); |
| 1121 | $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); | 1908 | $('#persona_description_position').on('input', onPersonaDescriptionPositionInput); |
| @@ -1151,55 +1938,41 @@ export function initPersonas() { | |||
| 1151 | $('#avatar_upload_file').on('change', changeUserAvatar); | 1938 | $('#avatar_upload_file').on('change', changeUserAvatar); |
| 1152 | 1939 | ||
| 1153 | $(document).on('click', '#user_avatar_block .avatar-container', function () { | 1940 | $(document).on('click', '#user_avatar_block .avatar-container', function () { |
| 1154 | const imgfile = $(this).attr('imgfile'); | 1941 | const imgfile = $(this).attr('data-avatar-id'); |
| 1155 | setUserAvatar(imgfile); | 1942 | setUserAvatar(imgfile); |
| 1156 | 1943 | ||
| 1157 | // force firstMes {{user}} update on persona switch | 1944 | // force firstMes {{user}} update on persona switch |
| 1158 | retriggerFirstMessageOnEmptyChat(); | 1945 | retriggerFirstMessageOnEmptyChat(); |
| 1159 | }); | 1946 | }); |
| 1160 | 1947 | ||
| 1161 | $('#your_name_button').click(async function () { | 1948 | $('#persona_rename_button').on('click', () => 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 | $(document).on('click', '#user_avatar_block .avatar_upload', function () { | 1950 | $(document).on('click', '#user_avatar_block .avatar_upload', function () { |
| 1169 | $('#avatar_upload_overwrite').val(''); | 1951 | $('#avatar_upload_overwrite').val(''); |
| 1170 | $('#avatar_upload_file').trigger('click'); | 1952 | $('#avatar_upload_file').trigger('click'); |
| 1171 | }); | 1953 | }); |
| 1172 | 1954 | ||
| 1173 | $(document).on('click', '#user_avatar_block .duplicate_persona', function (e) { | 1955 | $('#persona_duplicate_button').on('click', () => duplicatePersona(user_avatar)); |
| 1174 | e.stopPropagation(); | ||
| 1175 | const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile'); | ||
| 1176 | 1956 | ||
| 1177 | if (!avatarId) { | 1957 | $('#persona_set_image_button').on('click', function () { |
| 1958 | if (!user_avatar) { | ||
| 1178 | console.log('no imgfile'); | 1959 | console.log('no imgfile'); |
| 1179 | return; | 1960 | return; |
| 1180 | } | 1961 | } |
| 1181 | 1962 | ||
| 1182 | duplicatePersona(avatarId); | 1963 | $('#avatar_upload_overwrite').val(user_avatar); |
| 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 | |||
| 1189 | if (!avatarId) { | ||
| 1190 | console.log('no imgfile'); | ||
| 1191 | return; | ||
| 1192 | } | ||
| 1193 | |||
| 1194 | $('#avatar_upload_overwrite').val(avatarId); | ||
| 1195 | $('#avatar_upload_file').trigger('click'); | 1964 | $('#avatar_upload_file').trigger('click'); |
| 1196 | }); | 1965 | }); |
| 1197 | 1966 | ||
| 1967 | $('#char_connections_button').on('click', showCharConnections); | ||
| 1968 | |||
| 1198 | eventSource.on('charManagementDropdown', (target) => { | 1969 | eventSource.on('charManagementDropdown', (target) => { |
| 1199 | if (target === 'convert_to_persona') { | 1970 | if (target === 'convert_to_persona') { |
| 1200 | convertCharacterToPersona(); | 1971 | convertCharacterToPersona(); |
| 1201 | } | 1972 | } |
| 1202 | }); | 1973 | }); |
| 1203 | eventSource.on(event_types.CHAT_CHANGED, setChatLockedPersona); | 1974 | eventSource.on(event_types.CHAT_CHANGED, updatePersonaUIStates); |
| 1975 | eventSource.on(event_types.CHAT_CHANGED, loadPersonaForCurrentChat); | ||
| 1204 | switchPersonaGridView(); | 1976 | switchPersonaGridView(); |
| 1205 | } | 1977 | } |
| 1978 | |||
| @@ -1489,6 +1489,8 @@ async function loadPowerUserSettings(settings, data) { | |||
| 1489 | $('#custom_stopping_strings_macro').prop('checked', power_user.custom_stopping_strings_macro); | 1489 | $('#custom_stopping_strings_macro').prop('checked', power_user.custom_stopping_strings_macro); |
| 1490 | $('#fuzzy_search_checkbox').prop('checked', power_user.fuzzy_search); | 1490 | $('#fuzzy_search_checkbox').prop('checked', power_user.fuzzy_search); |
| 1491 | $('#persona_show_notifications').prop('checked', power_user.persona_show_notifications); | 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 | $('#encode_tags').prop('checked', power_user.encode_tags); | 1494 | $('#encode_tags').prop('checked', power_user.encode_tags); |
| 1493 | $('#example_messages_behavior').val(getExampleMessagesBehavior()); | 1495 | $('#example_messages_behavior').val(getExampleMessagesBehavior()); |
| 1494 | $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true); | 1496 | $(`#example_messages_behavior option[value="${getExampleMessagesBehavior()}"]`).prop('selected', true); |
| @@ -3705,6 +3707,16 @@ $(document).ready(() => { | |||
| 3705 | saveSettingsDebounced(); | 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 | $('#encode_tags').on('input', async function () { | 3720 | $('#encode_tags').on('input', async function () { |
| 3709 | power_user.encode_tags = !!$(this).prop('checked'); | 3721 | power_user.encode_tags = !!$(this).prop('checked'); |
| 3710 | await reloadCurrentChat(); | 3722 | await reloadCurrentChat(); |
| @@ -38,7 +38,6 @@ import { | |||
| 38 | setCharacterId, | 38 | setCharacterId, |
| 39 | setCharacterName, | 39 | setCharacterName, |
| 40 | setExtensionPrompt, | 40 | setExtensionPrompt, |
| 41 | setUserName, | ||
| 42 | showMoreMessages, | 41 | showMoreMessages, |
| 43 | stopGeneration, | 42 | stopGeneration, |
| 44 | substituteParams, | 43 | substituteParams, |
| @@ -55,7 +54,7 @@ import { getContext, saveMetadataDebounced } from './extensions.js'; | |||
| 55 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; | 54 | import { getRegexedString, regex_placement } from './extensions/regex/engine.js'; |
| 56 | import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; | 55 | import { findGroupMemberId, groups, is_group_generating, openGroupById, resetSelectedGroup, saveGroupChat, selected_group } from './group-chats.js'; |
| 57 | import { chat_completion_sources, oai_settings, promptManager } from './openai.js'; | 56 | import { chat_completion_sources, oai_settings, promptManager } from './openai.js'; |
| 58 | import { autoSelectPersona, retriggerFirstMessageOnEmptyChat, setPersonaLockState, togglePersonaLock, user_avatar } from './personas.js'; | 57 | import { user_avatar } from './personas.js'; |
| 59 | import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; | 58 | import { addEphemeralStoppingString, chat_styles, flushEphemeralStoppingStrings, power_user } from './power-user.js'; |
| 60 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; | 59 | import { SERVER_INPUTS, textgen_types, textgenerationwebui_settings } from './textgen-settings.js'; |
| 61 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; | 60 | import { decodeTextTokens, getAvailableTokenizers, getFriendlyTokenizerName, getTextTokens, getTokenCountAsync, selectTokenizer } from './tokenizers.js'; |
| @@ -124,46 +123,6 @@ export function initDefaultSlashCommands() { | |||
| 124 | helpString: 'Get help on macros, chat formatting and commands.', | 123 | helpString: 'Get help on macros, chat formatting and commands.', |
| 125 | })); | 124 | })); |
| 126 | SlashCommandParser.addCommandObject(SlashCommand.fromProps({ | 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 | name: 'bg', | 126 | name: 'bg', |
| 168 | callback: setBackgroundCallback, | 127 | callback: setBackgroundCallback, |
| 169 | aliases: ['background'], | 128 | aliases: ['background'], |
| @@ -3439,31 +3398,6 @@ export async function generateSystemMessage(_, prompt) { | |||
| 3439 | return ''; | 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 | function setStoryModeCallback() { | 3401 | function setStoryModeCallback() { |
| 3468 | $('#chat_display').val(chat_styles.DOCUMENT).trigger('change'); | 3402 | $('#chat_display').val(chat_styles.DOCUMENT).trigger('change'); |
| 3469 | return ''; | 3403 | return ''; |
| @@ -3479,48 +3413,6 @@ function setFlatModeCallback() { | |||
| 3479 | return ''; | 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 | async function setNarratorName(_, text) { | 3416 | async function setNarratorName(_, text) { |
| 3525 | const name = text || NARRATOR_NAME_DEFAULT; | 3417 | const name = text || NARRATOR_NAME_DEFAULT; |
| 3526 | chat_metadata[NARRATOR_NAME_KEY] = name; | 3418 | chat_metadata[NARRATOR_NAME_KEY] = name; |
| @@ -485,8 +485,8 @@ export function getTagKeyForEntityElement(element) { | |||
| 485 | } | 485 | } |
| 486 | // Start with the given element and traverse up the DOM tree | 486 | // Start with the given element and traverse up the DOM tree |
| 487 | while (element.length && element.parent().length) { | 487 | while (element.length && element.parent().length) { |
| 488 | const grid = element.attr('grid'); | 488 | const grid = element.attr('data-grid'); |
| 489 | const chid = element.attr('chid'); | 489 | const chid = element.attr('data-chid'); |
| 490 | if (grid || chid) { | 490 | if (grid || chid) { |
| 491 | const id = grid || chid; | 491 | const id = grid || chid; |
| 492 | return getTagKeyForEntity(id); | 492 | return getTagKeyForEntity(id); |
| @@ -2256,3 +2256,39 @@ export function arraysEqual(a, b) { | |||
| 2256 | } | 2256 | } |
| 2257 | return true; | 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 | if (characters[chid]?.data?.character_book) { | 4751 | if (characters[chid]?.data?.character_book) { |
| 4752 | $('#import_character_info').data('chid', chid).show(); | 4752 | $('#import_character_info').data('data-chid', chid).show(); |
| 4753 | 4753 | ||
| 4754 | // Only show the alert once per character | 4754 | // Only show the alert once per character |
| 4755 | const checkKey = `AlertWI_${characters[chid].avatar}`; | 4755 | const checkKey = `AlertWI_${characters[chid].avatar}`; |
| @@ -4783,7 +4783,7 @@ export function checkEmbeddedWorld(chid) { | |||
| 4783 | } | 4783 | } |
| 4784 | 4784 | ||
| 4785 | export async function importEmbeddedWorldInfo(skipPopup = false) { | 4785 | export async function importEmbeddedWorldInfo(skipPopup = false) { |
| 4786 | const chid = $('#import_character_info').data('chid'); | 4786 | const chid = $('#import_character_info').data('data-chid'); |
| 4787 | 4787 | ||
| 4788 | if (chid === undefined) { | 4788 | if (chid === undefined) { |
| 4789 | return; | 4789 | return; |
| @@ -228,6 +228,37 @@ table.responsiveTable { | |||
| 228 | animation-name: flash; | 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 | /* Keyboard/focus navigation styling */ | 262 | /* Keyboard/focus navigation styling */ |
| 232 | /* Mimic the outline of keyboard navigation for most most focusable controls */ | 263 | /* Mimic the outline of keyboard navigation for most most focusable controls */ |
| 233 | .interactable { | 264 | .interactable { |
| @@ -1339,6 +1370,8 @@ body.swipeAllMessages .mes:not(.last_mes) .swipes-counter { | |||
| 1339 | } | 1370 | } |
| 1340 | 1371 | ||
| 1341 | .avatars_inline { | 1372 | .avatars_inline { |
| 1373 | display: flex; | ||
| 1374 | gap: 5px; | ||
| 1342 | flex-wrap: wrap; | 1375 | flex-wrap: wrap; |
| 1343 | overflow: hidden; | 1376 | overflow: hidden; |
| 1344 | max-height: calc(var(--avatar-base-height) + 2 * var(--avatar-base-border-radius)); | 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 | margin-right: calc(var(--avatar-base-border-radius)); | 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 | .mes_block { | 1416 | .mes_block { |
| 1379 | padding-top: 0; | 1417 | padding-top: 0; |
| 1380 | padding-left: 10px; | 1418 | padding-left: 10px; |
| @@ -2869,6 +2907,7 @@ select option:not(:checked) { | |||
| 2869 | .menu_button.disabled { | 2907 | .menu_button.disabled { |
| 2870 | filter: brightness(75%) grayscale(1); | 2908 | filter: brightness(75%) grayscale(1); |
| 2871 | opacity: 0.5; | 2909 | opacity: 0.5; |
| 2910 | cursor: inherit; | ||
| 2872 | pointer-events: none; | 2911 | pointer-events: none; |
| 2873 | } | 2912 | } |
| 2874 | 2913 | ||
| @@ -2943,8 +2982,7 @@ select option:not(:checked) { | |||
| 2943 | } | 2982 | } |
| 2944 | 2983 | ||
| 2945 | #form_character_search_form .menu_button, | 2984 | #form_character_search_form .menu_button, |
| 2946 | #GroupFavDelOkBack .menu_button, | 2985 | #GroupFavDelOkBack .menu_button { |
| 2947 | .avatar-container .menu_button { | ||
| 2948 | margin: 0; | 2986 | margin: 0; |
| 2949 | height: fit-content; | 2987 | height: fit-content; |
| 2950 | padding: 5px; | 2988 | padding: 5px; |
| @@ -2978,8 +3016,22 @@ select option:not(:checked) { | |||
| 2978 | width: max-content; | 3016 | width: max-content; |
| 2979 | } | 3017 | } |
| 2980 | 3018 | ||
| 2981 | #persona-management-block .menu_button { | 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 | input[type=search]::-webkit-search-cancel-button { | 3037 | input[type=search]::-webkit-search-cancel-button { |
| @@ -3304,6 +3356,10 @@ input[type=search]:focus::-webkit-search-cancel-button { | |||
| 3304 | color: var(--golden); | 3356 | color: var(--golden); |
| 3305 | } | 3357 | } |
| 3306 | 3358 | ||
| 3359 | .avatar.is_active { | ||
| 3360 | outline: 2px solid var(--active); | ||
| 3361 | } | ||
| 3362 | |||
| 3307 | #fav_chara_wrap { | 3363 | #fav_chara_wrap { |
| 3308 | display: flex; | 3364 | display: flex; |
| 3309 | margin: 5px 0px; | 3365 | margin: 5px 0px; |
| @@ -3371,28 +3427,6 @@ grammarly-extension { | |||
| 3371 | z-index: 35; | 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 | .avatar_div .avatar { | 3430 | .avatar_div .avatar { |
| 3397 | /* margin-left: 4px; | 3431 | /* margin-left: 4px; |
| 3398 | margin-right: 10px; | 3432 | margin-right: 10px; |
| @@ -3455,12 +3489,7 @@ grammarly-extension { | |||
| 3455 | cursor: pointer; | 3489 | cursor: pointer; |
| 3456 | } | 3490 | } |
| 3457 | 3491 | ||
| 3458 | #rm_characters_block .form_create_bottom_buttons_block { | 3492 | .buttons_block { |
| 3459 | justify-content: space-evenly !important; | ||
| 3460 | flex-grow: 0; | ||
| 3461 | } | ||
| 3462 | |||
| 3463 | .form_create_bottom_buttons_block { | ||
| 3464 | display: flex; | 3493 | display: flex; |
| 3465 | flex: 1; | 3494 | flex: 1; |
| 3466 | gap: 5px; | 3495 | gap: 5px; |
| @@ -3469,18 +3498,21 @@ grammarly-extension { | |||
| 3469 | flex-wrap: wrap; | 3498 | flex-wrap: wrap; |
| 3470 | } | 3499 | } |
| 3471 | 3500 | ||
| 3472 | .form_create_bottom_buttons_block .menu_button { | 3501 | .buttons_block .menu_button { |
| 3473 | display: flex; | 3502 | display: flex; |
| 3474 | justify-content: center; | 3503 | justify-content: center; |
| 3475 | align-items: center; | 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 | .redWarningBG { | 3511 | .redWarningBG { |
| 3480 | background-color: var(--crimson70a) !important; | 3512 | background-color: var(--crimson70a) !important; |
| 3481 | } | 3513 | } |
| 3482 | 3514 | ||
| 3483 | #delete_button:hover, | 3515 | .red_button:hover, |
| 3484 | .redWarningBG:hover { | 3516 | .redWarningBG:hover { |
| 3485 | background-color: var(--fullred) !important; | 3517 | background-color: var(--fullred) !important; |
| 3486 | } | 3518 | } |
| @@ -3655,6 +3687,7 @@ grammarly-extension { | |||
| 3655 | 3687 | ||
| 3656 | .menu_button { | 3688 | .menu_button { |
| 3657 | color: var(--SmartThemeBodyColor); | 3689 | color: var(--SmartThemeBodyColor); |
| 3690 | filter: grayscale(0.5); | ||
| 3658 | background-color: var(--SmartThemeBlurTintColor); | 3691 | background-color: var(--SmartThemeBlurTintColor); |
| 3659 | border: 1px solid var(--SmartThemeBorderColor); | 3692 | border: 1px solid var(--SmartThemeBorderColor); |
| 3660 | border-radius: 5px; | 3693 | border-radius: 5px; |
| @@ -3674,15 +3707,8 @@ grammarly-extension { | |||
| 3674 | min-width: calc(1.25em + 12px); | 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 | background-color: var(--white30a); | 3712 | background-color: var(--white30a); |
| 3687 | } | 3713 | } |
| 3688 | 3714 | ||
| @@ -3931,10 +3957,39 @@ input[type='checkbox'].del_checkbox { | |||
| 3931 | outline: var(--avatar-base-border-radius) solid var(--golden); | 3957 | outline: var(--avatar-base-border-radius) solid var(--golden); |
| 3932 | } | 3958 | } |
| 3933 | 3959 | ||
| 3934 | .avatar-container.default_persona .set_default_persona { | 3960 | .avatar-container .avatar_state .fa-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 | color: var(--golden); | 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 | #user_avatar_block .avatar_upload { | 3993 | #user_avatar_block .avatar_upload { |
| 3939 | cursor: pointer; | 3994 | cursor: pointer; |
| 3940 | width: 60px; | 3995 | width: 60px; |
| @@ -5958,3 +6013,40 @@ body:not(.movingUI) .drawer-content.maximized { | |||
| 5958 | min-width: fit-content; | 6013 | min-width: fit-content; |
| 5959 | width: 40%; | 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 | } | ||