Merge branch 'dev' of https://github.com/BlipRanger/SillyTavern into feature/random

This commit is contained in:
BlipRanger
2023-06-27 20:18:55 -04:00
26 changed files with 1922 additions and 804 deletions

View File

@ -4,3 +4,4 @@ npm-debug.log
readme*
Start.bat
/dist
/backups/

1
.gitignore vendored
View File

@ -20,3 +20,4 @@ whitelist.txt
secrets.json
/dist
poe_device.json
/backups/

View File

@ -5,3 +5,4 @@ node_modules/
secrets.json
/dist
poe_device.json
/backups/

6
package-lock.json generated
View File

@ -10,6 +10,7 @@
"license": "AGPL-3.0",
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"@mlc-ai/web-tokenizers": "^0.1.0",
"axios": "^1.4.0",
"command-exists": "^1.2.9",
"compression": "^1",
@ -561,6 +562,11 @@
"integrity": "sha512-XPSJHWmi394fuUuzDnGz1wiKqWfo1yXecHQMRf2l6hztTO+nPru658AyDngaBe7isIxEkRsPR3FZh+s7iVa4Uw==",
"dev": true
},
"node_modules/@mlc-ai/web-tokenizers": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/@mlc-ai/web-tokenizers/-/web-tokenizers-0.1.0.tgz",
"integrity": "sha512-whiQ+40ohtAFoFOGcje1Io7BMr434Wh3hM3nBCWlJMpXxL5Rlig/AH9wjyUPsytKwWTEe7RoYPyXSbFw5Vs6Tw=="
},
"node_modules/@nodelib/fs.scandir": {
"version": "2.1.5",
"resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz",

View File

@ -1,6 +1,7 @@
{
"dependencies": {
"@dqbd/tiktoken": "^1.0.2",
"@mlc-ai/web-tokenizers": "^0.1.0",
"axios": "^1.4.0",
"command-exists": "^1.2.9",
"compression": "^1",

View File

@ -1,73 +0,0 @@
body {
margin: 0;
padding: 0;
width: 100%;
background-color: rgb(36, 37, 37);
background-repeat: no-repeat;
background-attachment: fixed;
background-size: cover;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
font-size: 16px;
/*1rem*/
color: #999;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
/*z-index:0;*/
}
#main {
padding-top: 20px;
/*z-index:1;*/
}
#content {
margin: 0 auto;
max-width: 700px;
border: 1px solid #333;
padding: 20px;
border-radius: 20px;
background-color: rgba(0, 0, 0, 0.5);
line-height: 1.5rem;
box-shadow: 0 0 5px black;
/*z-index: 2;*/
}
code {
border: 1px solid #999;
background-color: rgba(0, 0, 0, 0.5);
padding: 5px;
border-radius: 5px;
display: block;
white-space: pre-line;
}
a {
color: orange;
text-decoration: none;
border-bottom: 1px dotted orange;
}
h2,
h3 {
color: #ccc;
}
hr {
border: 1px solid #999;
}
table {
width: 100%;
}
table,
th,
td {
border: 1px solid;
border-collapse: collapse;
}
table img {
max-width: 200px;
}

1
public/css/select2.min.css vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -15,6 +15,7 @@
<link href="css/bright.min.css" rel="stylesheet">
<link href="css/cropper.min.css" rel="stylesheet">
<link href="css/toastr.min.css" rel="stylesheet">
<link href="css/select2.min.css" rel="stylesheet">
<link rel="apple-touch-icon" sizes="57x57" href="img/apple-icon-57x57.png" />
<link rel="apple-touch-icon" sizes="72x72" href="img/apple-icon-72x72.png" />
@ -35,6 +36,7 @@
<script src="scripts/jquery-cropper.min.js"></script>
<script src="scripts/toastr.min.js"></script>
<script src="scripts/fuse.js"></script>
<script src="scripts/select2.min.js"></script>
<script type="module" src="scripts/eventemitter.js"></script>
<script type="module" src="scripts/power-user.js"></script>
<script type="module" src="scripts/swiped-events.js"></script>
@ -1875,18 +1877,18 @@
</h3>
</div>
<div id="wi-holder" class="margin5">
<div class="wi-settings flex-container gap10px alignitemscenter">
<div class="flex1 flex-container flexFlowColumn">
<div class="flex range-block">
<div class="range-block-title justifyLeft">
<small>Active World</small>
</div>
<div class="range-block-range">
<select id="world_info" class="flexGrow margin0">
<option value="">--- None ---</option>
</select>
</div>
<div class="justifyContentSpaceAround wi-settings flex-container gap10px alignitemscenter">
<div id="WIMultiSelector" class="flex2 flex alignSelfStart range-block">
<div class="range-block-title justifyLeft">
<small>Active World(s)</small>
</div>
<div class="range-block-range">
<select id="world_info" multiple>
<option value="">-- World Info not found --</option>
</select>
</div>
</div>
<div class="flex2 flex-container flexFlowColumn">
<div class="flex range-block">
<div class="range-block-title justifyLeft">
<label for="world_info_character_strategy">
@ -1901,42 +1903,43 @@
</select>
</div>
</div>
</div>
<div name="WIScanAndTokens" class="flex1 flex-container flexFlowColumn">
<div class="flex1 gap5px range-block">
<div class="wide10pMinFit">
<small data-i18n="Scan Depth">Scan Depth</small>
</div>
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_depth" name="volume" min="0" max="10" step="1">
<div name="WIScanAndTokens" class="flex1 flex-container flexFlowColumn">
<div class="flex1 gap5px range-block">
<div class="wide10pMinFit">
<small data-i18n="Scan Depth">Scan Depth</small>
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_depth" id="world_info_depth_counter">
depth
<div class="range-block-range-and-counter">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_depth" name="volume" min="0" max="10" step="1">
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_depth" id="world_info_depth_counter">
depth
</div>
</div>
</div>
</div>
</div>
<div class="flex1 gap5px range-block">
<div class="wide10pMinFit">
<small data-i18n="Token Budget">Context %</small>
</div>
<div class="range-block-range-and-counter ">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
<div class="flex1 gap5px range-block">
<div class="wide10pMinFit">
<small data-i18n="Token Budget">Context %</small>
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_budget" id="world_info_budget_counter">
budget
<div class="range-block-range-and-counter ">
<div class="range-block-range paddingLeftRight5">
<input type="range" id="world_info_budget" name="volume" min="1" max="100" step="1">
</div>
<div class="range-block-counter margin0">
<div contenteditable="true" data-for="world_info_budget" id="world_info_budget_counter">
budget
</div>
</div>
</div>
</div>
</div>
</div>
<div class="range-block flex-container flexFlowColumn">
<div class="flex1 range-block flex-container flexFlowColumn">
<label title="Entries can activate other entries by mentioning their keywords" class="checkbox_label">
<input id="world_info_recursive" type="checkbox" />
<small>
@ -2000,7 +2003,7 @@
<div id="user-settings-button" class="drawer">
<div class="drawer-toggle">
<div class="drawer-icon fa-solid fa-face-smile closedIcon" title="User Settings"></div>
<div class="drawer-icon fa-solid fa-user-cog closedIcon" title="User Settings"></div>
</div>
<div id="user-settings-block" class="drawer-content closedDrawer">
<div class="flex-container wide100p alignitemscenter spaceBetween">
@ -2008,7 +2011,7 @@
<div id="version_display"></div>
</div>
<div class="flex-container spaceEvenly">
<div name="UI Customization" class="flex-container drawer25pWidth">
<div name="UI Customization" class="flex-container drawer33pWidth">
<div class="ui-settings">
<h4><span data-i18n="UI Customization">UI Customization</span></h4>
<div>
@ -2033,7 +2036,7 @@
<span data-i18n="Bubbles">Bubbles</span>
</label>
</div>
<div>
<div id="sheldWidthToggleBlock">
<span data-i18n="Chat Width (PC):">Chat Width (PC):</span><br>
<label>
<input name="sheld_width" type="radio" value="0" />
@ -2092,18 +2095,30 @@
<span data-i18n="Characters Hotswap">Characters Hotswap</span>
</label>
<label for="movingUImode" class="checkbox_label">
<label id="movingUIModeCheckBlock" for="movingUImode" class="checkbox_label">
<input id="movingUImode" type="checkbox" />
<span data-i18n="Movable UI Panels">Movable UI Panels</span>
</label>
<div class="flex-container flexFlowColumn">
<h4 data-i18n="Send on Enter">
Send on Enter
</h4>
<select id="send_on_enter">
<option value="-1"><span data-i18n="Always disabled">Always disabled</span></option>
<option value="0"><span data-i18n="Automatic (desktop)">Automatic (desktop)</span>
</option>
<option value="1"><span data-i18n="Always enabled">Always enabled</span></option>
</select>
</div>
<div id="movingUIreset" class="menu_button whitespacenowrap" data-i18n="Reset Panels">
Reset Panels</div>
</div>
</div>
</div>
<div id="UI-Theme-Block" class="flex-container flexFlowColumn drawer25pWidth">
<div id="UI-Theme-Block" class="flex-container flexFlowColumn drawer33pWidth">
<div id="color-picker-block" class="flex-container flexFlowColumn">
<h4><span data-i18n="UI Colors">UI Colors</span></h4>
<div class="flex-container">
@ -2200,7 +2215,7 @@
</div>
</div>
<div id="power-user-options-block" class="flex-container drawer25pWidth">
<div id="power-user-options-block" class="flex-container drawer33pWidth">
<div id="power-user-option-checkboxes">
<h4 data-i18n="Power User Options">Power User Options</h4>
<label for="swipes-checkbox">
@ -2249,8 +2264,32 @@
</a>
</label>
<label for="never_resize_avatars"><input id="never_resize_avatars" type="checkbox" />
<span data-i18n="Never Resize Avatars">Never Resize Avatars</span>
<span data-i18n="Never resize avatars">Never resize avatars</span>
</label>
<label for="show_card_avatar_urls"><input id="show_card_avatar_urls" type="checkbox" />
<span data-i18n="Show avatar filenames">Show avatar filenames</span>
</label>
<div class="inline-drawer wide100p flexFlowColumn">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Auto-swipe</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="auto_swipe">
<input id="auto_swipe" type="checkbox" />
Enabled
</label>
<div>Minimum generated message length</div>
<input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole">
<div>Blacklisted words</div>
<div class="auto_swipe">
<textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" placeholder="words you dont want generated separated by comma ','" class="text_pole textarea_compact" maxlength="5000" value="" autocomplete="off" rows="3"></textarea>
<div>Blacklisted word count to swipe</div>
<input id="auto_swipe_blacklist_threshold" name="auto_swipe_blacklist_threshold" type="number" min="0" step="1" value="1" class="text_pole">
</div>
</div>
</div>
<div id="reload_chat" class="menu_button whitespacenowrap" data-i18n="Reload Chat">
Reload Chat
@ -2258,68 +2297,6 @@
</div>
</div>
<div name="NameAndAvatar" class="flex-container flexFlowColumn drawer25pWidth">
<div class="inline-drawer wide100p flexFlowColumn">
<div class="flex-container flexFlowColumn">
<h4 data-i18n="Send on Enter">
Send on Enter
</h4>
<select id="send_on_enter">
<option value="-1"><span data-i18n="Always disabled">Always disabled</span></option>
<option value="0"><span data-i18n="Automatic (desktop)">Automatic (desktop)</span>
</option>
<option value="1"><span data-i18n="Always enabled">Always enabled</span></option>
</select>
</div>
<div class="inline-drawer-toggle inline-drawer-header">
<b>Auto-swipe</b>
<div class="fa-solid fa-circle-chevron-down inline-drawer-icon down"></div>
</div>
<div class="inline-drawer-content">
<label class="checkbox_label" for="auto_swipe">
<input id="auto_swipe" type="checkbox" />
Enabled
</label>
<div>Minimum generated message length</div>
<input id="auto_swipe_minimum_length" name="auto_swipe_minimum_length" type="number" min="0" step="1" value="0" class="text_pole">
<div>Blacklisted words</div>
<div class="auto_swipe">
<textarea id="auto_swipe_blacklist" name="auto_swipe_blacklist" placeholder="words you dont want generated separated by comma ','" class="text_pole textarea_compact" maxlength="5000" value="" autocomplete="off" rows="3"></textarea>
<div>Blacklisted word count to swipe</div>
<input id="auto_swipe_blacklist_threshold" name="auto_swipe_blacklist_threshold" type="number" min="0" step="1" value="1" class="text_pole">
</div>
</div>
</div>
<div name="NameChanger">
<h4 data-i18n="Name">Name</h4>
<div class="change_name">
<input id="your_name" name="your_name" placeholder="Enter your name" class="text_pole wide100p" maxlength="50" value="" autocomplete="off">
<div id="your_name_button" class="menu_button fa-solid fa-check" title="Click to set a new User Name">
</div>
<div id="lock_user_name" class="menu_button fa-solid fa-lock" title="Click to lock your selected persona to the current chat. Click again to remove the lock.">
</div>
<div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages">
</div>
</div>
</div>
<div name="AvatarSelector">
<h4 data-i18n="Your Avatar">Your Persona</h4>
<div id="user_avatar_block">
<div class="avatar_upload">+</div>
</div>
<form id="form_upload_avatar" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input type="file" id="avatar_upload_file" accept="image/*" name="avatar">
</form>
</div>
</div>
</div>
</div>
</div>
@ -2382,12 +2359,66 @@
</div>
</div>
<div id="persona-management-button" class="drawer">
<div class="drawer-toggle">
<div class="drawer-icon fa-solid fa-face-smile closedIcon" title="Persona Management"></div>
</div>
<div class="drawer-content closedDrawer">
<div class="flex-container wide100p alignitemscenter spaceBetween">
<h3><span data-i18n="Persona Management">Persona Management</span></h3>
<div id="persona-management-block" class="flex-container wide100p">
<div class="flex1">
<h4 data-i18n="Name">Name</h4>
<div class="change_name">
<input id="your_name" name="your_name" placeholder="Enter your name" class="text_pole wide100p" maxlength="50" value="" autocomplete="off">
<div id="your_name_button" class="menu_button fa-solid fa-check" title="Click to set a new User Name">
</div>
<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.">
</div>
<div id="sync_name_button" class="menu_button fa-solid fa-sync" title="Click to set user name for all messages">
</div>
</div>
<div>
<h4 data-i18n="Name">Persona Description</h4>
<textarea id="persona_description" name="persona_description" placeholder="Example:&#10;[{{user}} is a 28-year-old Romanian cat girl.]" class="text_pole textarea_compact" maxlength="5000" value="" autocomplete="off" rows="4"></textarea>
<label for="persona_description_position">Position:</label>
<select id="persona_description_position">
<option value="0">Before Character Card</option>
<option value="1">After Character Card</option>
<option value="2">Top of Author's Note</option>
<option value="3">Bottom of Author's Note</option>
</select>
</div>
</div>
<div class="flex1">
<h4 data-i18n="Your Avatar" class="title_restorable">
<span>Your Persona</span>
<div id="create_dummy_persona" class="menu_button menu_button_icon" title="Create a dummy persona">
<i class="fa-solid fa-person-circle-question fa-fw"></i>
<span>Blank</span>
</div>
</h4>
<div id="user_avatar_block">
<div class="avatar_upload">+</div>
</div>
<form id="form_upload_avatar" action="javascript:void(null);" method="post" enctype="multipart/form-data">
<input type="file" id="avatar_upload_file" accept="image/*" name="avatar">
<input type="hidden" id="avatar_upload_overwrite" name="overwrite_name" value="">
</form>
</div>
</div>
</div>
</div>
</div>
<div id="rightNavHolder" class="drawer">
<div id="unimportantYes" class="drawer-toggle drawer-header">
<div id="rightNavDrawerIcon" class="drawer-icon fa-solid fa-address-card closedIcon" title="Character Management">
</div>
</div>
<nav id="right-nav-panel" class="drawer-content closedDrawer fillRight gap5px">
<nav id="right-nav-panel" class="drawer-content closedDrawer fillRight">
<div id="right-nav-panelheader" class="fa-solid fa-grip drag-grabber">
</div>
@ -2429,70 +2460,76 @@
<input id="character_name_pole" name="ch_name" class="text_pole" placeholder="Name this character" maxlength="50" value="" autocomplete="off">
</div>
<div id="result_info" class="flex-container" title="Token counts may be inaccurate and provided just for reference.">&nbsp;</div>
<div id="avatar_div" class="avatar_div alignitemsflexstart justifySpaceBetween flexnowrap flexGap5">
<label id="avatar_div_div" class="add_avatar avatar" for="add_avatar_button" title="Click to select a new avatar for this character">
<img id="avatar_load_preview" src="img/ai4.png" alt="avatar">
<input hidden type="file" id="add_avatar_button" name="avatar" accept="image/png, image/jpeg, image/jpg, image/gif, image/bmp">
</label>
<div class="flex-container flexFlowColumn">
<label for="char-management-dropdown">
<select id="char-management-dropdown">
<option value="default" disabled selected>Options</option>
<option id="set_character_world">
<i class="fa-solid fa-globe"></i> Link to World Info
</option>
<option id="import_character_info">
<i class="fa-solid fa-file-import"></i> Import Embedded World Info
</option>
<option id="set_chat_scenario">
Scenario Override
</option>
<option id="renameCharButton">
Rename
</option>
<option id="dupe_button">
Duplicate
</option>
<option id="export_button">
Export
</option>
<option id="delete_button">
Delete
</option>
</select>
</label>
<div class="flex-container flexFlowColumn">
<div class="flex-container justifyContentFlexEnd">
<div class="form_create_bottom_buttons_block flexnowrap">
<div id="rm_button_back" class="menu_button fa-solid fa-left-long "></div>
<div class="form_create_bottom_buttons_block">
<div id="rm_button_back" class="menu_button fa-solid fa-left-long "></div>
<!-- <div id="renameCharButton" class="menu_button fa-solid fa-user-pen" title="Rename Character"></div> -->
<div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites"></div>
<input type="hidden" id="fav_checkbox" name="fav" />
<div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions"></div>
<div id="world_button" class="menu_button fa-solid fa-globe" title="Character Lore"></div>
<div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download"></div>
<!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
<!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->
<div id="dupe_button" class="menu_button fa-solid fa-clone " title="Duplicate Character"></div>
<label for="create_button" id="create_button_label" class="menu_button fa-solid fa-user-check" title="Create Character">
<input type="submit" id="create_button" name="create_button">
</label>
<div id="delete_button" class="menu_button fa-solid fa-skull " title="Delete Character"></div>
</div>
<label class="flex1" for="char-management-dropdown">
<select id="char-management-dropdown">
<option value="default" disabled selected>More...</option>
<option id="set_character_world">
<i class="fa-solid fa-globe"></i> Link to World Info
</option>
<option id="import_character_info">
<i class="fa-solid fa-file-import"></i> Import Card Lore
</option>
<option id="set_chat_scenario">
Scenario Override
</option>
<option id="renameCharButton">
Rename
</option>
<!--<option id="dupe_button">
Duplicate
</option>
<option id="export_button">
Export
</option>
<option id="delete_button">
Delete
</option>-->
</select>
</label>
</div>
<!-- <div id="renameCharButton" class="menu_button fa-solid fa-user-pen" title="Rename Character"></div> -->
<div id="favorite_button" class="menu_button fa-solid fa-star" title="Add to Favorites"></div>
<input type="hidden" id="fav_checkbox" name="fav" />
<div id="advanced_div" class="menu_button fa-solid fa-book " title="Advanced Definitions"></div>
<!-- <div id="export_button" class="menu_button fa-solid fa-file-export " title="Export and Download"></div> -->
<!-- <div id="set_chat_scenario" class="menu_button fa-solid fa-scroll" title="Set a chat scenario override"></div> -->
<!-- <div id="set_character_world" class="menu_button fa-solid fa-globe" title="Set a character World Info / Lorebook"></div> -->
<!-- <div id="dupe_button" class="menu_button fa-solid fa-clone " title="Duplicate Character"></div> -->
<label for="create_button" id="create_button_label" class="menu_button fa-solid fa-user-check" title="Create Character">
<input type="submit" id="create_button" name="create_button">
</label>
<!-- <div id="delete_button" class="menu_button fa-solid fa-skull " title="Delete Character"></div> -->
<div id="tags_div" class="marginBot5">
<div class="tag_controls">
<input id="tagInput" class="text_pole tag_input wide100p margin0" placeholder="Search / Create tags" maxlength="25" />
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags"></div>
</div>
<div id="tagList" class="tags"></div>
</div>
</div>
<div id="result_info" class="justifyCenter flex-container" title="Token counts may be inaccurate and provided just for reference.">&nbsp;</div>
</div>
</div>
</div>
<hr>
<div id="tags_div" class="marginBot5">
<div class="tag_controls">
<input id="tagInput" class="text_pole tag_input wide100p margin0" placeholder="Search / Create tags" maxlength="25" />
<div class="tags_view menu_button fa-solid fa-tags" title="View all tags"></div>
</div>
<div id="tagList" class="tags"></div>
</div>
<div id="description_div" class="marginBot5">
<span data-i18n="Description">Description</span>
@ -2548,8 +2585,8 @@
<div id="groupTagList" class="tags paddingTopBot5"></div>
</div>
<div id="rm_group_top_bar" class="flex-container alignitemscenter spaceBetween width100p">
<div class="flex1 flex-container alignitemscenter justifyCenter">
<label class="add_avatar avatar" for="group_avatar_button" title="Click to select a new avatar for this group">
<div>
<label class="add_avatar avatar flex-container justifyCenter" for="group_avatar_button" title="Click to select a new avatar for this group">
<div id="group_avatar_preview">
<div class="avatar">
<img src="img/ai4.png" alt="avatar">
@ -2641,6 +2678,7 @@
<form id="form_character_search_form" action="javascript:void(null);">
<div id="rm_button_create" title="Create New Character" class="menu_button fa-solid fa-user-plus "></div>
<div id="character_import_button" title="Import Character from File" class="menu_button fa-solid fa-file-arrow-up faSmallFontSquareFix"></div>
<div id="external_import_button" title="Import content from external URL" class="menu_button fa-solid fa-cloud-arrow-down faSmallFontSquareFix"></div>
<div id="rm_button_group_chats" title="Create New Chat Group" class="menu_button fa-solid fa-users-gear "></div>
<input id="character_search_bar" class="text_pole width100p" type="search" placeholder="Search..." maxlength="50" />
<select id="character_sort_order" title="Characters sorting order">
@ -2693,7 +2731,7 @@
<div id="character_popup_text">
<h3 id="character_popup_text_h3"></h3> <span data-i18n="Advanced Defininitions">- Advanced
Defininitions</span>
Definitions</span>
</div>
<hr class="margin-bot-10px">
<div id="character_cross" class="fa-solid fa-circle-xmark"></div>
@ -2895,16 +2933,32 @@
Select a World Info file for <span class="character_name"></span>:
</h3>
</div>
<h4>Primary Lorebook</h4>
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
A selected World Info / Lorebook will be bound to this character.
When generating an AI reply, it will be combined with the entries from a global World Info / Lorebook selector.
Exporting a character would also export the selected World Info file embedded in the JSON data.
A selected World Info will be bound to this character as its own Lorebook.
When generating an AI reply, it will be combined with the entries from a global World Info selector.
Exporting a character would also export the selected Lorebook file embedded in the JSON data.
</div>
<div class="range-block-range wide100p">
<select class="character_world_info_selector">
<select class="character_world_info_selector wide100p">
<option value="">--- None ---</option>
</select>
</div>
<div class="range-block-title">
<h4>
Additional Lorebooks
</h4>
</div>
<div class="range-block-counter justifyLeft flex-container flexFlowColumn margin-bot-10px">
Associate one or more auxillary Lorebooks with this character.<br>
NOTE: These choices are optional and won't be preserved on character export!
</div>
<div class="range-block-range wide100p">
<select class="character_extra_world_info_selector wide100p" multiple>
<option value="">-- World Info not found --</option>
</select>
</div>
</div>
</div>
@ -3103,7 +3157,7 @@
<img src="">
</div>
<div class="flex-container wide100pLess70px">
<div class="ch_name"></div>
<div class="wide100p"><span class="ch_name"></span> <i class="ch_avatar_url"></i></div>
<i class="ch_fav_icon fa-solid fa-star"></i>
<input class="ch_fav" value="" hidden />
<div class="ch_description"></div>
@ -3346,10 +3400,13 @@
<div class="export_format list-group-item" data-format="webp">WEBP</div>
</div>
<div id="avatar_zoom_popup">
<div id="avatar_zoom_popupheader" class="fa-solid fa-grip drag-grabber"></div>
<img id="zoomed_avatar" src="">
<div id="zoomed_avatar_template" class="template_element">
<div class="zoomed_avatar">
<div id="" class="fa-solid fa-grip drag-grabber"></div>
<img class="zoomed_avatar_img" src="">
</div>
</div>
<div id="rawPromptPopup" class="list-group">
<div id="rawPromptWrapper" class="tokenItemizingSubclass"></div>
</div>
@ -3368,8 +3425,8 @@
</button>
</div>
<div class="avatar-buttons avatar-buttons-bottom">
<button class="menu_button set_user_info" title="Under construction">
<i class="fa-solid fa-circle-user"></i>
<button class="menu_button set_persona_image" title="Change persona image">
<i class="fa-solid fa-image"></i>
</button>
<button class="menu_button delete_avatar" title="Delete persona">

View File

@ -1,23 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>SillyTavern Documentation</title>
<link rel="stylesheet" href="/css/notes.css">
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="/webfonts/NotoSans/stylesheet.css" rel="stylesheet">
</head>
<body>
<div id="main">
<div id="content">
<h2>You weren't supposed to be able to get here, you know.</h1>
<h3>All help materials has been moved here:</h3>
<h3><a href="https://docs.sillytavern.app/">SillyTavern Documentation</a></h3>
</div>
</div>
</body>
</html>

View File

@ -1,4 +1,4 @@
import { humanizedDateTime, favsToHotswap, getMessageTimeStamp } from "./scripts/RossAscends-mods.js";
import { humanizedDateTime, favsToHotswap, getMessageTimeStamp, dragElement, isMobile } from "./scripts/RossAscends-mods.js";
import { encode } from "../scripts/gpt-2-3-tokenizer/mod.js";
import { GPT3BrowserTokenizer } from "../scripts/gpt-3-tokenizer/gpt3-tokenizer.js";
import {
@ -30,6 +30,9 @@ import {
world_names,
world_info_character_strategy,
importEmbeddedWorldInfo,
checkEmbeddedWorld,
setWorldInfoButtonClass,
importWorldInfo,
} from "./scripts/world-info.js";
import {
@ -69,6 +72,8 @@ import {
formatInstructModeChat,
formatInstructStoryString,
formatInstructModePrompt,
persona_description_positions,
loadMovingUIState,
} from "./scripts/power-user.js";
import {
@ -128,6 +133,7 @@ import {
timestampToMoment,
download,
isDataURL,
getCharaFilename,
} from "./scripts/utils.js";
import { extension_settings, loadExtensionSettings, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
@ -151,7 +157,8 @@ import {
import { EventEmitter } from './scripts/eventemitter.js';
import { context_settings, loadContextTemplatesFromSettings } from "./scripts/context-template.js";
import { markdownExclusionExt } from "./scripts/showdown-exclusion.js";
import { setFloatingPrompt } from "./scripts/extensions/floating-prompt/index.js";
import { NOTE_MODULE_NAME, metadata_keys, setFloatingPrompt, shouldWIAddPrompt } from "./scripts/extensions/floating-prompt/index.js";
import { deviceInfo } from "./scripts/RossAscends-mods.js";
//exporting functions and vars for mods
export {
@ -222,6 +229,7 @@ export {
extension_prompt_types,
updateVisibleDivs,
mesForShowdownParse,
printCharacters,
}
// API OBJECT FOR EXTERNAL WIRING
@ -274,7 +282,7 @@ let is_mes_reload_avatar = false;
let optionsPopper = Popper.createPopper(document.getElementById('options_button'), document.getElementById('options'), {
placement: 'top-start'
});
let exportPopper = Popper.createPopper(document.getElementById('char-management-dropdown'), document.getElementById('export_format_popup'), {
let exportPopper = Popper.createPopper(document.getElementById('export_button'), document.getElementById('export_format_popup'), {
placement: 'left'
});
let rawPromptPopper = Popper.createPopper(document.getElementById('dialogue_popup'), document.getElementById('rawPromptPopup'), {
@ -287,13 +295,14 @@ let streamingProcessor = null;
let crop_data = undefined;
let is_delete_mode = false;
let fav_ch_checked = false;
let scrollLock = false;
//initialize global var for future cropped blobs
let currentCroppedAvatar = '';
const durationSaveEdit = 1000;
const saveSettingsDebounced = debounce(() => saveSettings(), durationSaveEdit);
const saveCharacterDebounced = debounce(() => $("#create_button").trigger('click'), durationSaveEdit);
export const saveCharacterDebounced = debounce(() => $("#create_button").trigger('click'), durationSaveEdit);
const getStatusDebounced = debounce(() => getStatus(), 300_000);
const saveChatDebounced = debounce(() => saveChatConditional(), durationSaveEdit);
@ -799,11 +808,14 @@ async function printCharacters() {
template.find('img').attr('src', this_avatar);
template.find('.avatar').attr('title', item.avatar);
template.find('.ch_name').text(item.name);
if (power_user.show_card_avatar_urls) {
template.find('.ch_avatar_url').text(item.avatar);
}
template.find('.ch_fav_icon').css("display", 'none');
template.toggleClass('is_fav', item.fav || item.fav == 'true');
template.find('.ch_fav').val(item.fav);
const description = item.data?.creator_notes || '';
const description = item.data?.creator_notes?.split('\n', 1)[0] || '';
if (description) {
template.find('.ch_description').text(description);
}
@ -1008,6 +1020,10 @@ function clearChat() {
count_view_mes = 0;
extension_prompts = {};
$("#chat").children().remove();
if ($('.zoomed_avatar[forChar]').length) {
console.debug('saw avatars to remove')
$('.zoomed_avatar[forChar]').remove();
} else { console.debug('saw no avatars') }
itemizedPrompts = [];
}
@ -1373,8 +1389,18 @@ function formatGenerationTimer(gen_started, gen_finished) {
function scrollChatToBottom() {
if (power_user.auto_scroll_chat_to_bottom) {
var $textchat = $("#chat");
$textchat.scrollTop(($textchat[0].scrollHeight));
const chatElement = $("#chat");
let position = chatElement[0].scrollHeight;
if (power_user.waifuMode) {
const lastMessage = chatElement.find('.mes').last();
if (lastMessage.length) {
const lastMessagePosition = lastMessage.position().top;
position = chatElement.scrollTop() + lastMessagePosition;
}
}
chatElement.scrollTop(position);
}
}
@ -1555,6 +1581,28 @@ function cleanGroupMessage(getMessage) {
return getMessage;
}
function getPersonaDescription(storyString) {
if (!power_user.persona_description) {
return storyString;
}
switch (power_user.persona_description_position) {
case persona_description_positions.BEFORE_CHAR:
return `${substituteParams(power_user.persona_description)}\n${storyString}`;
case persona_description_positions.AFTER_CHAR:
return `${storyString}\n${substituteParams(power_user.persona_description)}`;
default:
if (shouldWIAddPrompt) {
const originalAN = extension_prompts[NOTE_MODULE_NAME].value
const ANWithDesc = persona_description_positions.TOP_AN
? `${power_user.persona_description}\n${originalAN}`
: `${originalAN}\n${power_user.persona_description}`;
setExtensionPrompt(NOTE_MODULE_NAME, ANWithDesc, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
}
return storyString;
}
}
function getAllExtensionPrompts() {
const value = Object
.values(extension_prompts)
@ -1719,7 +1767,9 @@ class StreamingProcessor {
this.setFirstSwipe(messageId);
}
scrollChatToBottom();
if (!scrollLock) {
scrollChatToBottom();
}
}
onFinishStreaming(messageId, text) {
@ -1813,6 +1863,7 @@ class StreamingProcessor {
if (this.messageId == -1) {
this.messageId = this.onStartStreaming(this.firstMessageText);
await delay(1); // delay for message to be rendered
scrollLock = false;
}
try {
@ -1999,12 +2050,6 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
console.log(`Core/all messages: ${coreChat.length}/${chat.length}`);
}
if (main_api === 'openai') {
message_already_generated = ''; // OpenAI doesn't have multigen
setOpenAIMessages(coreChat);
setOpenAIMessageExamples(mesExamplesArray);
}
let storyString = "";
if (is_pygmalion) {
@ -2034,7 +2079,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
let chat2 = [];
for (let i = coreChat.length - 1, j = 0; i >= 0; i--, j++) {
// For OpenAI it's only used in WI
if (main_api == 'openai' && !world_info) {
if (main_api == 'openai' && (!world_info || world_info.length === 0)) {
console.debug('No WI, skipping chat2 for OAI');
break;
}
@ -2063,11 +2108,19 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
setFloatingPrompt();
// Add WI to prompt (and also inject WI to AN value via hijack)
let { worldInfoString, worldInfoBefore, worldInfoAfter } = await getWorldInfoPrompt(chat2, this_max_context);
// Add persona description to prompt
storyString = getPersonaDescription(storyString);
// Call combined AN into Generate
let allAnchors = getAllExtensionPrompts();
const afterScenarioAnchor = getExtensionPrompt(extension_prompt_types.AFTER_SCENARIO);
let zeroDepthAnchor = getExtensionPrompt(extension_prompt_types.IN_CHAT, 0, ' ');
if (main_api === 'openai') {
message_already_generated = ''; // OpenAI doesn't have multigen
setOpenAIMessages(coreChat);
setOpenAIMessageExamples(mesExamplesArray);
}
// Moved here to not overflow the Poe context with added prompt bits
if (main_api == 'poe') {
allAnchors = appendPoeAnchors(type, allAnchors);
@ -3955,8 +4008,6 @@ function changeMainAPI() {
////////////////////////////////////////////////////
async function getUserAvatars() {
$("#user_avatar_block").html(""); //RossAscends: necessary to avoid doubling avatars each refresh.
$("#user_avatar_block").append('<div class="avatar_upload">+</div>');
const response = await fetch("/getuseravatars", {
method: "POST",
headers: getRequestHeaders(),
@ -3968,6 +4019,8 @@ async function getUserAvatars() {
const getData = await response.json();
//background = getData;
//console.log(getData.length);
$("#user_avatar_block").html(""); //RossAscends: necessary to avoid doubling avatars each refresh.
$("#user_avatar_block").append('<div class="avatar_upload">+</div>');
for (var i = 0; i < getData.length; i++) {
//console.log(1);
@ -3978,6 +4031,56 @@ async function getUserAvatars() {
}
}
function setPersonaDescription() {
$("#persona_description").val(power_user.persona_description);
$("#persona_description_position")
.val(power_user.persona_description_position)
.find(`option[value='${power_user.persona_description_position}']`)
.attr("selected", true);
}
function onPersonaDescriptionPositionInput() {
power_user.persona_description_position = Number(
$("#persona_description_position").find(":selected").val()
);
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: power_user.persona_description_position,
};
power_user.persona_descriptions[user_avatar] = object;
}
object.position = power_user.persona_description_position;
}
saveSettingsDebounced();
}
function onPersonaDescriptionInput() {
power_user.persona_description = $("#persona_description").val();
if (power_user.personas[user_avatar]) {
let object = power_user.persona_descriptions[user_avatar];
if (!object) {
object = {
description: power_user.persona_description,
position: Number($("#persona_description_position").find(":selected").val()),
};
power_user.persona_descriptions[user_avatar] = object;
}
object.description = power_user.persona_description;
}
saveSettingsDebounced();
}
function highlightSelectedAvatar() {
$("#user_avatar_block").find(".avatar").removeClass("selected");
$("#user_avatar_block")
@ -4000,12 +4103,15 @@ function appendUserAvatar(name) {
highlightSelectedAvatar();
}
function reloadUserAvatar() {
function reloadUserAvatar(force = false) {
$(".mes").each(function () {
const avatarImg = $(this).find(".avatar img");
if (force) {
avatarImg.attr("src", avatarImg.attr("src"));
}
if ($(this).attr("is_user") == 'true' && $(this).attr('force_avatar') == 'false') {
$(this)
.find(".avatar img")
.attr("src", getUserAvatar(user_avatar));
avatarImg.attr("src", getUserAvatar(user_avatar));
}
});
}
@ -4054,9 +4160,20 @@ async function bindUserNameToPersona() {
// If the user clicked ok and entered a name, bind the name to the persona
console.log(`Binding persona ${avatarId} to name ${personaName}`);
power_user.personas[avatarId] = personaName;
const descriptor = power_user.persona_descriptions[avatarId];
const isCurrentPersona = avatarId === user_avatar;
// Create a description object if it doesn't exist
if (!descriptor) {
// If the user is currently using this persona, set the description to the current description
power_user.persona_descriptions[avatarId] = {
description: isCurrentPersona ? power_user.persona_description : '',
position: isCurrentPersona ? power_user.persona_description_position : persona_description_positions.BEFORE_CHAR,
};
}
// If the user is currently using this persona, update the name
if (avatarId === user_avatar) {
if (isCurrentPersona) {
console.log(`Auto-updating user name to ${personaName}`);
setUserName(personaName);
}
@ -4064,16 +4181,39 @@ async function bindUserNameToPersona() {
// If the user clicked ok, but didn't enter a name, delete the persona
console.log(`Unbinding persona ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
}
saveSettingsDebounced();
await getUserAvatars();
setPersonaDescription();
}
async function createDummyPersona() {
const fetchResult = await fetch(default_avatar);
const blob = await fetchResult.blob();
const file = new File([blob], "avatar.png", { type: "image/png" });
const formData = new FormData();
formData.append("avatar", file);
jQuery.ajax({
type: "POST",
url: "/uploaduseravatar",
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function (data) {
await getUserAvatars();
},
});
}
function updateUserLockIcon() {
const hasLock = !!chat_metadata['persona'];
$('#lock_user_name').toggleClass('fa-lock', !hasLock);
$('#lock_user_name').toggleClass('fa-unlock', hasLock);
$('#lock_user_name').toggleClass('fa-unlock', !hasLock);
$('#lock_user_name').toggleClass('fa-lock', hasLock);
}
function setUserAvatar() {
@ -4094,12 +4234,75 @@ function setUserAvatar() {
}
setUserName(personaName);
const descriptor = power_user.persona_descriptions[user_avatar];
if (descriptor) {
power_user.persona_description = descriptor.description;
power_user.persona_description_position = descriptor.position;
} else {
power_user.persona_description = '';
power_user.persona_description_position = persona_description_positions.BEFORE_CHAR;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.BEFORE_CHAR };
}
setPersonaDescription();
}
}
async function setUserInfo() {
// TODO Replace with actual implementation
callPopup('This functionality is under development.<br>Please check back later.', 'text');
async function uploadUserAvatar(e) {
const file = e.target.files[0];
if (!file) {
$("#form_upload_avatar").trigger("reset");
return;
}
const formData = new FormData($("#form_upload_avatar").get(0));
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(file);
});
$('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
const confirmation = await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop');
if (!confirmation) {
return;
}
let url = "/uploaduseravatar";
if (crop_data !== undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function () {
// If the user uploaded a new avatar, we want to make sure it's not cached
const name = formData.get("overwrite_name");
if (name) {
await fetch(getUserAvatar(name), { cache: "no-cache" });
reloadUserAvatar(true);
}
crop_data = undefined;
await getUserAvatars();
},
error: (jqXHR, exception) => { },
});
// Will allow to select the same file twice in a row
$("#form_upload_avatar").trigger("reset");
}
async function setDefaultPersona() {
@ -4162,7 +4365,7 @@ async function deleteUserAvatar() {
return;
}
const confirm = await callPopup('Are you sure you want to delete this avatar?', 'confirm');
const confirm = await callPopup('<h3>Are you sure you want to delete this avatar?</h3>All information associated with its linked persona will be lost.', 'confirm');
if (!confirm) {
console.debug('User cancelled deleting avatar');
@ -4180,6 +4383,7 @@ async function deleteUserAvatar() {
if (request.ok) {
console.log(`Deleted avatar ${avatarId}`);
delete power_user.personas[avatarId];
delete power_user.persona_descriptions[avatarId];
if (avatarId === power_user.default_persona) {
toastr.warning('The default persona was deleted. You will need to set a new default persona.', 'Default persona deleted');
@ -4216,6 +4420,7 @@ function lockUserNameToChat() {
{ timeOut: 10000, extendedTimeOut: 20000, },
);
power_user.personas[user_avatar] = name1;
power_user.persona_descriptions[user_avatar] = { description: '', position: persona_description_positions.BEFORE_CHAR };
}
chat_metadata['persona'] = user_avatar;
@ -4413,6 +4618,7 @@ async function getSettings(type) {
user_avatar = settings.user_avatar;
reloadUserAvatar();
highlightSelectedAvatar();
setPersonaDescription();
//Load the API server URL from settings
api_server = settings.api_server;
@ -4875,39 +5081,13 @@ export function select_selected_character(chid) {
$("#renameCharButton").css("display", "");
$('.open_alternate_greetings').data('chid', chid);
$('#set_character_world').data('chid', chid);
const world = characters[chid]?.data?.extensions?.world;
const worldSet = Boolean(world && world_names.includes(world));
$('#set_character_world').toggleClass('world_set', worldSet);
setWorldInfoButtonClass(chid);
checkEmbeddedWorld(chid);
$("#form_create").attr("actiontype", "editcharacter");
saveSettingsDebounced();
}
function checkEmbeddedWorld(chid) {
$('#import_character_info').hide();
if (chid === undefined) {
return;
}
if (characters[chid]?.data?.character_book) {
$('#import_character_info').data('chid', chid).show();
// Only show the alert once per character
const checkKey = `AlertWI_${characters[chid].avatar}`;
const worldName = characters[chid]?.data?.extensions?.world;
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
toastr.info(
'To import and use it, select "Import Embedded World Info" in the Options dropdown menu on the character panel.',
`${characters[chid].name} has an embedded World/Lorebook`,
{ timeOut: 10000, extendedTimeOut: 20000, positionClass: 'toast-top-center' },
);
localStorage.setItem(checkKey, 1);
}
}
}
function select_rm_create() {
menu_type = "create";
@ -4956,7 +5136,7 @@ function select_rm_create() {
$("#name_div").addClass('displayBlock');
$('.open_alternate_greetings').data('chid', undefined);
$('#set_character_world').data('chid', undefined);
$('#set_character_world').toggleClass('world_set', !!create_save.world);
setWorldInfoButtonClass(undefined, !!create_save.world);
updateFavButtonState(false);
checkEmbeddedWorld();
@ -5339,7 +5519,7 @@ function openCharacterWorldPopup() {
}
function onSelectCharacterWorld() {
const value = $(this).find('option:selected').val();
const value = $('.character_world_info_selector').find('option:selected').val();
const worldIndex = value !== '' ? Number(value) : NaN;
const name = !isNaN(worldIndex) ? world_names[worldIndex] : '';
@ -5353,15 +5533,45 @@ function openCharacterWorldPopup() {
createOrEditCharacter();
}
$('#set_character_world').toggleClass('world_set', !!value);
setWorldInfoButtonClass(undefined, !!value);
}
function onExtraWorldInfoChanged() {
const selectedWorlds = $('.character_extra_world_info_selector').val();
let charLore = world_info.charLore ?? [];
// TODO: Maybe make this utility function not use the window context?
const fileName = getCharaFilename(chid);
const tempExtraBooks = selectedWorlds.map((index) => world_names[index]).filter((e) => e !== undefined);
const existingCharLore = charLore.find((e) => e.name === fileName);
if (existingCharLore) {
if (tempExtraBooks.length === 0) {
charLore.splice(existingCharLore, 1);
} else {
existingCharLore.extraBooks = tempExtraBooks;
}
} else {
const newCharLoreEntry = {
name: fileName,
extraBooks: tempExtraBooks
}
charLore.push(newCharLoreEntry);
}
Object.assign(world_info, { charLore: charLore });
saveSettingsDebounced();
}
const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
const template = $('#character_world_template .character_world').clone();
const select = template.find('.character_world_info_selector');
const extraSelect = template.find('.character_extra_world_info_selector');
const name = (menu_type == 'create' ? create_save.name : characters[chid]?.data?.name) || 'Nameless';
const worldId = (menu_type == 'create' ? create_save.world : characters[chid]?.data?.extensions?.world) || '';
template.find('.character_name').text(name);
// Apped to base dropdown
world_names.forEach((item, i) => {
const option = document.createElement('option');
option.value = i;
@ -5370,7 +5580,47 @@ function openCharacterWorldPopup() {
select.append(option);
});
// Append to extras dropdown
if (world_names.length > 0) {
extraSelect.empty();
}
world_names.forEach((item, i) => {
const option = document.createElement('option');
option.value = i;
option.innerText = item;
const existingCharLore = world_info.charLore?.find((e) => e.name === getCharaFilename());
if (existingCharLore) {
option.selected = existingCharLore.extraBooks.includes(item);
} else {
option.selected = false;
}
extraSelect.append(option);
});
select.on('change', onSelectCharacterWorld);
extraSelect.on('mousedown change', async function (e) {
// If there's no world names, don't do anything
if (world_names.length === 0) {
e.preventDefault();
return;
}
let selectScrollTop = null;
if (deviceInfo && deviceInfo.device.type === 'desktop') {
e.preventDefault();
const option = $(e.target);
const selectElement = $(extraSelect)[0];
selectScrollTop = selectElement.scrollTop;
option.prop('selected', !option.prop('selected'));
await delay(1);
selectElement.scrollTop = selectScrollTop;
}
onExtraWorldInfoChanged();
});
callPopup(template, 'text');
}
@ -6078,7 +6328,13 @@ const isPwaMode = window.navigator.standalone;
if (isPwaMode) { $("body").addClass('PWA') }
$(document).ready(function () {
//////////INPUT BAR FOCUS-KEEPING LOGIC/////////////
if (isMobile() === true) {
console.debug('hiding movingUI and sheldWidth toggles for mobile')
$("#sheldWidthToggleBlock").hide();
$("#movingUIModeCheckBlock").hide();
}
registerSlashCommand('dupe', DupeChar, [], " duplicates the currently selected character", true, true);
registerSlashCommand('api', connectAPISlash, [], " connect to an API", true, true);
@ -6093,11 +6349,11 @@ $(document).ready(function () {
updateVisibleDivs('#rm_print_characters_block', true);
}, 5));
// This does not actually increase performance.
/*$("#chat").on('scroll', debounce(() => {
updateVisibleDivs('#chat', false);
}, 10));*/
$("#chat").on('mousewheel', () => {
scrollLock = true;
});
//////////INPUT BAR FOCUS-KEEPING LOGIC/////////////
let S_TAFocused = false;
let S_TAPreviouslyFocused = false;
$('#send_textarea').on('focusin focus click', () => {
@ -6113,7 +6369,8 @@ $(document).ready(function () {
});
$(document).click(event => {
if ($(':focus').attr('id') !== 'send_textarea') {
if (!$(event.target.id).is("#options_button, #send_but, #send_textarea, #option_regenerate")) {
var validIDs = ["options_button", "send_but", "send_textarea", "option_regenerate"];
if ($(event.target).attr('id') !== validIDs) {
S_TAFocused = false;
S_TAPreviouslyFocused = false;
}
@ -6265,56 +6522,21 @@ $(document).ready(function () {
$(document).on("click", "#user_avatar_block .avatar", setUserAvatar);
$(document).on("click", "#user_avatar_block .avatar_upload", function () {
$("#avatar_upload_file").click();
$("#avatar_upload_overwrite").val("");
$("#avatar_upload_file").trigger('click');
});
$("#avatar_upload_file").on("change", async function (e) {
const file = e.target.files[0];
$(document).on("click", "#user_avatar_block .set_persona_image", function () {
const avatarId = $(this).closest('.avatar-container').find('.avatar').attr('imgfile');
if (!file) {
if (!avatarId) {
console.log('no imgfile');
return;
}
const formData = new FormData($("#form_upload_avatar").get(0));
const dataUrl = await new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = resolve;
reader.onerror = reject;
reader.readAsDataURL(file);
});
$('#dialogue_popup').addClass('large_dialogue_popup wide_dialogue_popup');
const confirmation = await callPopup(getCropPopup(dataUrl.target.result), 'avatarToCrop');
if (!confirmation) {
return;
}
let url = "/uploaduseravatar";
if (crop_data !== undefined) {
url += `?crop=${encodeURIComponent(JSON.stringify(crop_data))}`;
}
jQuery.ajax({
type: "POST",
url: url,
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: function (data) {
if (data.path) {
appendUserAvatar(data.path);
}
crop_data = undefined;
},
error: (jqXHR, exception) => { },
});
// Will allow to select the same file twice in a row
$("#form_upload_avatar").trigger("reset");
$("#avatar_upload_overwrite").val(avatarId);
$("#avatar_upload_file").trigger('click');
});
$("#avatar_upload_file").on("change", uploadUserAvatar);
$(document).on("click", ".bg_example", async function () {
//when user clicks on a BG thumbnail...
@ -6558,17 +6780,17 @@ $(document).ready(function () {
$("#form_create").submit(createOrEditCharacter);
/* $("#delete_button").click(function () {
popup_type = "del_ch";
callPopup(`
$("#delete_button").on('click', function () {
popup_type = "del_ch";
callPopup(`
<h3>Delete the character?</h3>
<b>THIS IS PERMANENT!<br><br>
THIS WILL ALSO DELETE ALL<br>
OF THE CHARACTER'S CHAT FILES.<br><br></b>`
);
}); */
);
});
$("#rm_info_button").click(function () {
$("#rm_info_button").on('click', function () {
$("#rm_info_avatar").html("");
select_rm_characters();
});
@ -7281,6 +7503,8 @@ $(document).ready(function () {
setUserName($('#your_name').val());
});
$("#create_dummy_persona").on('click', createDummyPersona);
$('#sync_name_button').on('click', async function () {
const confirmation = await callPopup(`<h3>Are you sure?</h3>All user-sent messages in this chat will be attributed to ${name1}.`, 'confirm');
@ -7324,8 +7548,9 @@ $(document).ready(function () {
$(document).on('click', '.bind_user_name', bindUserNameToPersona);
$(document).on('click', '.delete_avatar', deleteUserAvatar);
$(document).on('click', '.set_default_persona', setDefaultPersona);
$(document).on('click', '.set_user_info', setUserInfo);
$('#lock_user_name').on('click', lockUserNameToChat);
$('#persona_description').on('input', onPersonaDescriptionInput);
$('#persona_description_position').on('input', onPersonaDescriptionPositionInput);
//**************************CHARACTER IMPORT EXPORT*************************//
$("#character_import_button").click(function () {
@ -7341,10 +7566,10 @@ $(document).ready(function () {
importCharacter(file);
}
});
/* $("#export_button").click(function (e) {
$('#export_format_popup').toggle();
exportPopper.update();
}); */
$("#export_button").on('click', function (e) {
$('#export_format_popup').toggle();
exportPopper.update();
});
$(document).on('click', '.export_format', async function () {
const format = $(this).data('format');
@ -7426,19 +7651,19 @@ $(document).ready(function () {
select_rm_characters();
});
/* $("#dupe_button").click(async function () {
$("#dupe_button").click(async function () {
const body = { avatar_url: characters[this_chid].avatar };
const response = await fetch('/dupecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success("Character Duplicated");
getCharacters();
}
}); */
const body = { avatar_url: characters[this_chid].avatar };
const response = await fetch('/dupecharacter', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify(body),
});
if (response.ok) {
toastr.success("Character Duplicated");
getCharacters();
}
});
$(document).on("click", ".select_chat_block, .bookmark_link, .mes_bookmark", async function () {
let file_name = $(this).hasClass('mes_bookmark')
@ -7476,12 +7701,15 @@ $(document).ready(function () {
$('.drawer-toggle').click(function () {
var icon = $(this).find('.drawer-icon');
var drawer = $(this).parent().find('.drawer-content');
if (drawer.hasClass('resizing')) { return }
var drawerWasOpenAlready = $(this).parent().find('.drawer-content').hasClass('openDrawer');
let targetDrawerID = $(this).parent().find('.drawer-content').attr('id');
const pinnedDrawerClicked = drawer.hasClass('pinnedOpen');
if (!drawerWasOpenAlready) {
$('.openDrawer').not('.pinnedOpen').slideToggle(200, "swing");
if (!drawerWasOpenAlready) { //to open the drawer
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, "swing", function () {
$(this).closest('.drawer-content').removeClass('resizing');
});
$('.openIcon').toggleClass('closedIcon openIcon');
$('.openDrawer').not('.pinnedOpen').toggleClass('closedDrawer openDrawer');
icon.toggleClass('openIcon closedIcon');
@ -7489,29 +7717,36 @@ $(document).ready(function () {
//console.log(targetDrawerID);
if (targetDrawerID === 'right-nav-panel') {
$(this).closest('.drawer').find('.drawer-content').slideToggle({
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle({
duration: 200,
easing: "swing",
start: function () {
jQuery(this).css('display', 'flex');
},
complete: function () {
$(this).closest('.drawer-content').removeClass('resizing');
$("#rm_print_characters_block").trigger("scroll");
}
})
} else {
$(this).closest('.drawer').find('.drawer-content').slideToggle(200, "swing");
$(this).closest('.drawer').find('.drawer-content').addClass('resizing').slideToggle(200, "swing", function () {
$(this).closest('.drawer-content').removeClass('resizing');
});
}
} else if (drawerWasOpenAlready) {
} else if (drawerWasOpenAlready) { //to close
icon.toggleClass('closedIcon openIcon');
if (pinnedDrawerClicked) {
$(drawer).slideToggle(200, "swing");
$(drawer).addClass('resizing').slideToggle(200, "swing", function () {
$(this).removeClass('resizing');
});
}
else {
$('.openDrawer').not('.pinnedOpen').slideToggle(200, "swing");
$('.openDrawer').not('.pinnedOpen').addClass('resizing').slideToggle(200, "swing", function () {
$(this).closest('.drawer-content').removeClass('resizing');
});
}
drawer.toggleClass('closedDrawer openDrawer');
@ -7566,22 +7801,44 @@ $(document).ready(function () {
});
$(document).on('click', '.mes .avatar', function () {
console.log(isMobile());
console.log($('body').hasClass('waifuMode'));
if (isMobile() === true && !$('body').hasClass('waifuMode')) {
console.debug('saw mobile regular mode, returning');
return;
} else { console.debug('saw valid env for zoomed display') }
let thumbURL = $(this).children('img').attr('src');
let charsPath = '/characters/'
let targetAvatarImg = thumbURL.substring(thumbURL.lastIndexOf("=") + 1);
let charname = targetAvatarImg.replace('.png', '');
let avatarSrc = isDataURL(thumbURL) ? thumbURL : charsPath + targetAvatarImg;
console.log(avatarSrc);
if ($(this).parent().parent().attr('is_user') == 'true') { //handle user avatars
$("#zoomed_avatar").attr('src', thumbURL);
} else if ($(this).parent().parent().attr('is_system') == 'true') { //handle system avatars
$("#zoomed_avatar").attr('src', thumbURL);
} else if ($(this).parent().parent().attr('is_user') == 'false') { //handle char avatars
$("#zoomed_avatar").attr('src', avatarSrc);
}
$('#avatar_zoom_popup').toggle();
if ($(`.zoomed_avatar[forChar="${charname}"]`).length) {
console.debug('removing container as it already existed')
$(`.zoomed_avatar[forChar="${charname}"]`).remove();
} else {
console.debug('making new container from template')
const template = $('#zoomed_avatar_template').html();
const newElement = $(template);
newElement.attr('forChar', charname);
newElement.attr('id', `zoomFor_${charname}`);
newElement.find('.drag-grabber').attr('id', `zoomFor_${charname}header`);
//} else { return; }
$('body').append(newElement);
if ($(this).parent().parent().attr('is_user') == 'true') { //handle user avatars
$(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL);
} else if ($(this).parent().parent().attr('is_system') == 'true') { //handle system avatars
$(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', thumbURL);
} else if ($(this).parent().parent().attr('is_user') == 'false') { //handle char avatars
$(`.zoomed_avatar[forChar="${charname}"] img`).attr('src', avatarSrc);
}
loadMovingUIState();
$(`.zoomed_avatar[forChar="${charname}"]`).css('display', 'block');
dragElement(newElement)
}
});
$(document).on('click', '#OpenAllWIEntries', function () {
@ -7636,18 +7893,19 @@ $(document).ready(function () {
case 'renameCharButton':
renameCharacter();
break;
case 'dupe_button':
/*case 'dupe_button':
DupeChar();
break;
case 'export_button':
$('#export_format_popup').toggle();
exportPopper.update();
break;
*/
case 'import_character_info':
await importEmbeddedWorldInfo();
saveCharacterDebounced();
break;
case 'delete_button':
/*case 'delete_button':
popup_type = "del_ch";
callPopup(`
<h3>Delete the character?</h3>
@ -7655,7 +7913,7 @@ $(document).ready(function () {
THIS WILL ALSO DELETE ALL<br>
OF THE CHARACTER'S CHAT FILES.<br><br></b>`
);
break;
break;*/
}
$("#char-management-dropdown").prop('selectedIndex', 0);
});
@ -7713,6 +7971,55 @@ $(document).ready(function () {
restoreCaretPosition($(this).get(0), caretPosition);
});
$('#external_import_button').on('click', async () => {
const html = `<h3>Enter the URL of the content to import</h3>
Supported sources:<br>
<ul class="justifyLeft">
<li>Chub characters (direct link or id)<br>Example: <tt>lorebooks/bartleby/example-lorebook</tt></li>
<li>Chub lorebooks (direct link or id)<br>Example: <tt>Anonymous/example-character</tt></li>
<li>More coming soon...</li>
<ul>`
const input = await callPopup(html, 'input');
if (!input) {
console.debug('Custom content import cancelled');
return;
}
const url = input.trim();
console.debug('Custom content import started', url);
const request = await fetch('/import_custom', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ url }),
});
if (!request.ok) {
toastr.info(request.statusText, 'Custom content import failed');
console.error('Custom content import failed', request.status, request.statusText);
return;
}
const data = await request.blob();
const customContentType = request.headers.get('X-Custom-Content-Type');
const fileName = request.headers.get('Content-Disposition').split('filename=')[1].replace(/"/g, '');
const file = new File([data], fileName, { type: data.type });
switch (customContentType) {
case 'character':
processDroppedFiles([file]);
break;
case 'lorebook':
await importWorldInfo(file);
break;
default:
toastr.warn('Unknown content type');
console.error('Unknown content type', customContentType);
break;
}
});
const $dropzone = $(document.body);
$dropzone.on('dragover', (event) => {

View File

@ -11,8 +11,8 @@ import {
is_send_press,
getTokenCount,
menu_type,
max_context,
saveSettingsDebounced,
} from "../script.js";
@ -287,18 +287,18 @@ export function RA_CountCharTokens() {
} else { console.debug("RA_TC -- no valid char found, closing."); }
}
// display the counted tokens
if (count_tokens < 1024 && perm_tokens < 1024) {
//display normal if both counts are under 1024
const tokenLimit = Math.max(((main_api !== 'openai' ? max_context : oai_settings.openai_max_context) / 2), 1024);
if (count_tokens < tokenLimit && perm_tokens < tokenLimit) {
$("#result_info").html(`<small>${count_tokens} Tokens (${perm_tokens} Permanent)</small>`);
} else {
$("#result_info").html(`
<div class="flex-container flexFlowColumn alignitemscenter">
<div class="flex-container alignitemscenter">
<div class="flex-container flexnowrap flexNoGap">
<small class="flex-container flexnowrap flexNoGap">
<div class="neutral_warning">${count_tokens}</div>&nbsp;Tokens (<div class="neutral_warning">${perm_tokens}</div><div>&nbsp;Permanent)</div>
</small>
</div>
<div id="chartokenwarning" class="menu_button whitespacenowrap"><a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-tokens" target="_blank">About Token 'Limits'</a></div>
<div id="chartokenwarning" class="menu_button margin0 whitespacenowrap"><a href="https://docs.sillytavern.app/usage/core-concepts/characterdesign/#character-tokens" target="_blank">About Token 'Limits'</a></div>
</div>`);
} //warn if either are over 1024
}
@ -473,60 +473,97 @@ function OpenNavPanels() {
// Make the DIV element draggable:
dragElement(document.getElementById("sheld"));
dragElement(document.getElementById("left-nav-panel"));
dragElement(document.getElementById("right-nav-panel"));
dragElement(document.getElementById("avatar_zoom_popup"));
dragElement(document.getElementById("WorldInfo"));
// SECOND UPDATE AIMING FOR MUTATIONS ONLY
export function dragElement(elmnt) {
var pos1 = 0, pos2 = 0, pos3 = 0, pos4 = 0;
if (document.getElementById(elmnt.id + "header")) { //ex: id="sheldheader"
// if present, the header is where you move the DIV from, but this overrides everything else:
document.getElementById(elmnt.id + "header").onmousedown = dragMouseDown;
var height, width, top, left;
var elmntName = elmnt.attr('id');
const elmntNameEscaped = $.escapeSelector(elmntName);
const elmntHeader = $(`#${elmntNameEscaped}header`);
if (elmntHeader.length) {
elmntHeader.off('mousedown').on('mousedown', (e) => {
dragMouseDown(e);
});
} else {
// otherwise, move the DIV from anywhere inside the DIV, b:
elmnt.onmousedown = dragMouseDown;
elmnt.off('mousedown').on('mousedown', dragMouseDown);
}
const observer = new MutationObserver((mutations) => {
const target = mutations[0].target;
if (!$(target).is(':visible')
|| $(target).hasClass('resizing')
|| Number((String(target.height).replace('px', ''))) < 50
|| Number((String(target.width).replace('px', ''))) < 50
|| isMobile() === true
) { return }
const style = getComputedStyle(target);
console.log(style.top, style.left)
height = target.offsetHeight;
width = target.offsetWidth;
top = parseInt(style.top);
left = parseInt(style.left);
/* console.log(`
height=${height},
width=${width},
top=${top},
left=${left}`); */
if (!power_user.movingUIState[elmntName]) {
power_user.movingUIState[elmntName] = {};
}
power_user.movingUIState[elmntName].top = top;
power_user.movingUIState[elmntName].left = left;
power_user.movingUIState[elmntName].width = width;
power_user.movingUIState[elmntName].height = height;
power_user.movingUIState[elmntName].right = 'unset';
power_user.movingUIState[elmntName].bottom = 'unset';
power_user.movingUIState[elmntName].margin = 'unset';
saveSettingsDebounced();
saveSettingsDebounced();
// Check if the element header exists and set the listener on the grabber
if (elmntHeader.length) {
elmntHeader.off('mousedown').on('mousedown', (e) => {
dragMouseDown(e);
});
} else {
elmnt.off('mousedown').on('mousedown', dragMouseDown);
}
});
observer.observe(elmnt.get(0), { attributes: true, attributeFilter: ['style'] });
function dragMouseDown(e) {
//console.log(e);
e = e || window.event;
e.preventDefault();
// get the mouse cursor position at startup:
pos3 = e.clientX; //mouse X at click
pos4 = e.clientY; //mouse Y at click
document.onmouseup = closeDragElement;
// call a function whenever the cursor moves:
document.onmousemove = elementDrag;
if (e) {
e.preventDefault();
pos3 = e.clientX; //mouse X at click
pos4 = e.clientY; //mouse Y at click
}
$(document).on('mouseup', closeDragElement);
$(document).on('mousemove', elementDrag);
}
function elementDrag(e) {
//disable scrollbars when dragging to prevent jitter
$("body").css("overflow", "hidden");
if (!power_user.movingUIState[elmntName]) {
power_user.movingUIState[elmntName] = {};
}
//get window size
let winWidth = window.innerWidth;
let winHeight = window.innerHeight;
//get necessary data for calculating element footprint
let draggableHeight = parseInt(getComputedStyle(elmnt).getPropertyValue('height').slice(0, -2));
let draggableWidth = parseInt(getComputedStyle(elmnt).getPropertyValue('width').slice(0, -2));
let draggableTop = parseInt(getComputedStyle(elmnt).getPropertyValue('top').slice(0, -2));
let draggableLeft = parseInt(getComputedStyle(elmnt).getPropertyValue('left').slice(0, -2));
let sheldWidth = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--sheldWidth').slice(0, -2));
let sheldWidth = parseInt($('html').css('--sheldWidth').slice(0, -2));
let topBarFirstX = (winWidth - sheldWidth) / 2;
let topBarLastX = topBarFirstX + sheldWidth;
let maxX = (width + left);
let maxY = (height + top);
//set the lowest and most-right pixel the element touches
let maxX = (draggableWidth + draggableLeft);
let maxY = (draggableHeight + draggableTop);
// calculate the new cursor position:
e = e || window.event;
e.preventDefault();
@ -535,88 +572,54 @@ export function dragElement(elmnt) {
pos3 = e.clientX; //new mouse X
pos4 = e.clientY; //new mouse Y
elmnt.setAttribute('data-dragged', 'true');
elmnt.attr('data-dragged', 'true');
//fix over/underflows:
setTimeout(function () {
if (elmnt.offsetTop < 40) {
/* console.log('6'); */
if (maxX > topBarFirstX && maxX < topBarLastX) {
/* console.log('maxX inside topBar!'); */
elmnt.style.top = "42px";
}
if (elmnt.offsetLeft < topBarLastX && elmnt.offsetLeft > topBarFirstX) {
/* console.log('offsetLeft inside TopBar!'); */
elmnt.style.top = "42px";
}
if (elmnt.offset().top < 40) {
if (maxX > topBarFirstX && maxX < topBarLastX) {
elmnt.css('top', '42px');
}
if (elmnt.offsetTop - pos2 <= 0) {
/* console.log('1'); */
//prevent going out of window top + 42px barrier for TopBar (can hide grabber)
elmnt.style.top = "0px";
if (elmnt.offset().left < topBarLastX && elmnt.offset().left > topBarFirstX) {
elmnt.css('top', '42px');
}
if (elmnt.offsetLeft - pos1 <= 0) {
/* console.log('2'); */
//prevent moving out of window left
elmnt.style.left = "0px";
}
if (elmnt.offset().top - pos2 <= 0) {
elmnt.css('top', '0px');
}
if (elmnt.offset().left - pos1 <= 0) {
elmnt.css('left', '0px');
}
if (maxX >= winWidth) {
elmnt.css('left', elmnt.offset().left - 10 + "px");
}
if (maxY >= winHeight) {
elmnt.css('top', elmnt.offset().top - 10 + "px");
if (elmnt.offset().top - pos2 <= 40) {
elmnt.css('top', '20px');
}
}
elmnt.css('left', (elmnt.offset().left - pos1) + "px");
elmnt.css("top", (elmnt.offset().top - pos2) + "px");
elmnt.css('margin', 'unset');
if (maxX >= winWidth) {
/* console.log('3'); */
//bounce off right
elmnt.style.left = elmnt.offsetLeft - 10 + "px";
}
if (maxY >= winHeight) {
/* console.log('4'); */
//bounce off bottom
elmnt.style.top = elmnt.offsetTop - 10 + "px";
if (elmnt.offsetTop - pos2 <= 40) {
/* console.log('5'); */
//prevent going out of window top + 42px barrier for TopBar (can hide grabber)
/* console.log('caught Y bounce to <40Y top'); */
elmnt.style.top = "20px";
}
}
// if no problems, set element's new position
/* console.log('7'); */
elmnt.style.left = (elmnt.offsetLeft - pos1) + "px";
elmnt.style.top = (elmnt.offsetTop - pos2) + "px";
$(elmnt).css("bottom", "unset");
$(elmnt).css("right", "unset");
$(elmnt).css("margin", "unset");
/* console.log(`
offsetLeft: ${elmnt.offsetLeft}, offsetTop: ${elmnt.offsetTop}
winWidth: ${winWidth}, winHeight: ${winHeight}
sheldWidth: ${sheldWidth}
X: ${elmnt.style.left}
Y: ${elmnt.style.top}
MaxX: ${maxX}, MaxY: ${maxY}
Topbar 1st X: ${((winWidth - sheldWidth) / 2)}
TopBar lastX: ${((winWidth - sheldWidth) / 2) + sheldWidth}
`); */
}, 50)
/* console.log("left/top: " + (elmnt.offsetLeft - pos1) + "/" + (elmnt.offsetTop - pos2) +
", win: " + winWidth + "/" + winHeight +
", max X / Y: " + maxX + " / " + maxY); */
/*
console.log(`
winWidth: ${winWidth}, winHeight: ${winHeight}
sheldWidth: ${sheldWidth}
X: ${$(elmnt).css('left')}
Y: ${$(elmnt).css('top')}
MaxX: ${maxX}, MaxY: ${maxY}
Topbar 1st X: ${((winWidth - sheldWidth) / 2)}
TopBar lastX: ${((winWidth - sheldWidth) / 2) + sheldWidth}
`);
*/
}
function closeDragElement() {
// stop moving when mouse button is released:
document.onmouseup = null;
document.onmousemove = null;
//revert scrolling to normal after drag to allow recovery of vastly misplaced elements
$("body").css("overflow", "auto");
$(document).off('mouseup', closeDragElement);
$(document).off('mousemove', elementDrag);
$("body").css("overflow", "");
// Clear the "data-dragged" attribute
elmnt.attr('data-dragged', 'false');
}
}
@ -626,7 +629,22 @@ export function dragElement(elmnt) {
$("document").ready(function () {
// initial status check
setTimeout(RA_checkOnlineStatus, 100);
setTimeout(() => {
if (isMobile === false) {
dragElement($("#sheld"));
dragElement($("#left-nav-panel"));
dragElement($("#right-nav-panel"));
dragElement($("#WorldInfo"));
dragElement($("#floatingPrompt"));
}
RA_checkOnlineStatus
}
, 100);
//$('div').on('resize', saveMovingUIState());
// read the state of AutoConnect and AutoLoadChat.
$(AutoConnectCheckbox).prop("checked", LoadLocalBool("AutoConnectEnabled"));

View File

@ -120,7 +120,7 @@ $(document).ready(function () {
<div class="background_settings">
<div class="inline-drawer">
<div class="inline-drawer-toggle inline-drawer-header">
<b>Character Backgrounds</b>
<b>Chat Backgrounds</b>
<div class="inline-drawer-icon fa-solid fa-circle-chevron-down down"></div>
</div>
<div class="inline-drawer-content">

View File

@ -1,5 +1,5 @@
{
"display_name": "Character Backgrounds",
"display_name": "Chat Backgrounds",
"loading_order": 7,
"requires": [],
"optional": [],

View File

@ -10,7 +10,7 @@ import { selected_group } from "../../group-chats.js";
import { ModuleWorkerWrapper, extension_settings, getContext, saveMetadataDebounced } from "../../extensions.js";
import { registerSlashCommand } from "../../slash-commands.js";
import { getCharaFilename, debounce } from "../../utils.js";
export { MODULE_NAME };
export { MODULE_NAME as NOTE_MODULE_NAME };
const MODULE_NAME = '2_floating_prompt'; // <= Deliberate, for sorting lower than memory
const UPDATE_INTERVAL = 1000;
@ -222,6 +222,8 @@ function loadSettings() {
export function setFloatingPrompt() {
const context = getContext();
if (!context.groupId && context.characterId === undefined) {
console.debug('setFloatingPrompt: Not in a chat. Skipping.');
shouldWIAddPrompt = false;
return;
}
@ -243,6 +245,7 @@ export function setFloatingPrompt() {
if (lastMessageNumber <= 0 || chat_metadata[metadata_keys.interval] <= 0) {
context.setExtensionPrompt(MODULE_NAME, '');
$('#extension_floating_counter').text('(disabled)');
shouldWIAddPrompt = false;
return;
}

View File

@ -702,7 +702,6 @@ jQuery(async () => {
<option value="date">Sort memories by date</option>
<option value="distance">Sort memories by relevance</option>
</select>
<label for="chromadb_keep_context">How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</label>
<label for="chromadb_keep_context"><small>How many original chat messages to keep: (<span id="chromadb_keep_context_value"></span>) messages</small></label>
<input id="chromadb_keep_context" type="range" min="${defaultSettings.keep_context_min}" max="${defaultSettings.keep_context_max}" step="${defaultSettings.keep_context_step}" value="${defaultSettings.keep_context}" />
<label for="chromadb_n_results"><small>Maximum number of ChromaDB 'memories' to inject: (<span id="chromadb_n_results_value"></span>) messages</small></label>

View File

@ -9,6 +9,7 @@ import {
updateMessageBlock,
} from "../../../script.js";
import { extension_settings, getContext } from "../../extensions.js";
import { secret_state, writeSecret } from "../../secrets.js";
const autoModeOptions = {
NONE: 'none',
@ -134,6 +135,14 @@ const languageCodes = {
'Zulu': 'zu',
};
const KEY_REQUIRED = ['deepl'];
function showKeyButton() {
const providerRequiresKey = KEY_REQUIRED.includes(extension_settings.translate.provider);
$("#translate_key_button").toggle(providerRequiresKey);
$("#translate_key_button").toggleClass('success', Boolean(secret_state[extension_settings.translate.provider]));
}
function loadSettings() {
for (const key in defaultSettings) {
if (!extension_settings.translate.hasOwnProperty(key)) {
@ -144,6 +153,7 @@ function loadSettings() {
$(`#translation_provider option[value="${extension_settings.translate.provider}"]`).attr('selected', true);
$(`#translation_target_language option[value="${extension_settings.translate.target_language}"]`).attr('selected', true);
$(`#translation_auto_mode option[value="${extension_settings.translate.auto_mode}"]`).attr('selected', true);
showKeyButton();
}
async function translateImpersonate(text) {
@ -186,18 +196,39 @@ async function translateProviderGoogle(text, lang) {
throw new Error(response.statusText);
}
async function translateProviderDeepl(text, lang) {
if (!secret_state.deepl) {
throw new Error('No DeepL API key');
}
const response = await fetch('/deepl_translate', {
method: 'POST',
headers: getRequestHeaders(),
body: JSON.stringify({ text: text, lang: lang }),
});
if (response.ok) {
const result = await response.text();
return result;
}
throw new Error(response.statusText);
}
async function translate(text, lang) {
try {
switch (extension_settings.translate.provider) {
case 'google':
return await translateProviderGoogle(text, lang);
case 'deepl':
return await translateProviderDeepl(text, lang);
default:
console.error('Unknown translation provider', extension_settings.translate.provider);
return text;
}
} catch (error) {
console.log(error);
toastr.error('Failed to translate message');
toastr.error(String(error), 'Failed to translate message');
}
}
@ -331,9 +362,13 @@ jQuery(() => {
<option value="both">Translate both</option>
</select>
<label for="translation_provider">Provider</label>
<select id="translation_provider" name="provider">
<option value="google">Google</option>
<select>
<div class="flex-container gap5px flexnowrap marginBot5">
<select id="translation_provider" name="provider" class="margin0">
<option value="google">Google</option>
<option value="deepl">DeepL</option>
<select>
<div id="translate_key_button" class="menu_button fa-solid fa-key margin0"></div>
</div>
<label for="translation_target_language">Target Language</label>
<select id="translation_target_language" name="target_language"></select>
<div id="translation_clear" class="menu_button">
@ -364,6 +399,7 @@ jQuery(() => {
});
$('#translation_provider').on('change', (event) => {
extension_settings.translate.provider = event.target.value;
showKeyButton();
saveSettingsDebounced();
});
$('#translation_target_language').on('change', (event) => {
@ -371,6 +407,17 @@ jQuery(() => {
saveSettingsDebounced();
});
$(document).on('click', '.mes_translate', onMessageTranslateClick);
$('#translate_key_button').on('click', async () => {
const optionText = $('#translation_provider option:selected').text();
const key = await callPopup(`<h3>${optionText} API Key</h3>`, 'input');
if (key == false) {
return;
}
await writeSecret(extension_settings.translate.provider, key);
toastr.success('API Key saved');
});
loadSettings();

View File

@ -2,6 +2,5 @@
width: fit-content;
display: flex;
gap: 10px;
align-items: baseline;
flex-direction: row;
}

View File

@ -225,12 +225,10 @@ async function showKudos() {
}
jQuery(function () {
let hordeModelSelectScrollTop = null;
$("#horde_model").on('mousedown change', async function (e) {
//desktop-only routine for multi-select without CTRL
if (deviceInfo.device.type === 'desktop') {
/*if (deviceInfo.device.type === 'desktop') {
let hordeModelSelectScrollTop = null;
e.preventDefault();
const option = $(e.target);
const selectElement = $(this)[0];
@ -238,7 +236,7 @@ jQuery(function () {
option.prop('selected', !option.prop('selected'));
await delay(1);
selectElement.scrollTop = hordeModelSelectScrollTop;
}
}*/
horde_settings.models = $('#horde_model').val();
console.log('Updated Horde models', horde_settings.models);
});
@ -265,4 +263,14 @@ jQuery(function () {
$("#horde_refresh").on("click", getHordeModels);
$("#horde_kudos").on("click", showKudos);
// Not needed on mobile
if (deviceInfo.device.type === 'desktop') {
$('#horde_model').select2({
width: '100%',
placeholder: 'Select Horde models',
allowClear: true,
closeOnSelect: false,
});
}
})

View File

@ -927,16 +927,15 @@ function getTokenizerModel() {
return turboTokenizer;
}
else if (oai_settings.windowai_model.includes('claude')) {
return turboTokenizer;
return 'claude';
}
else if (oai_settings.windowai_model.includes('GPT-NeoXT')) {
return 'gpt2';
}
}
// We don't have a Claude tokenizer for JS yet. Turbo 3.5 should be able to handle this.
if (oai_settings.chat_completion_source == chat_completion_sources.CLAUDE) {
return turboTokenizer;
return 'claude';
}
// Default to Turbo 3.5

View File

@ -12,12 +12,13 @@ import {
eventSource,
event_types,
getCurrentChatId,
printCharacters,
name1,
name2,
replaceCurrentChat,
setCharacterId
} from "../script.js";
import { favsToHotswap } from "./RossAscends-mods.js";
import { favsToHotswap, isMobile } from "./RossAscends-mods.js";
import {
groups,
selected_group,
@ -27,6 +28,7 @@ import { registerSlashCommand } from "./slash-commands.js";
export {
loadPowerUserSettings,
loadMovingUIState,
collapseNewlines,
playMessageSound,
sortGroupMembers,
@ -77,6 +79,13 @@ const send_on_enter_options = {
ENABLED: 1,
}
export const persona_description_positions = {
BEFORE_CHAR: 0,
AFTER_CHAR: 1,
TOP_AN: 2,
BOTTOM_AN: 3,
}
let power_user = {
tokenizer: tokenizers.CLASSIC,
token_padding: 64,
@ -101,6 +110,7 @@ let power_user = {
chat_display: chat_styles.DEFAULT,
sheld_width: sheld_width.DEFAULT,
never_resize_avatars: false,
show_card_avatar_urls: false,
play_message_sound: false,
play_sound_unfocused: true,
auto_save_msg_edits: false,
@ -122,6 +132,7 @@ let power_user = {
waifuMode: false,
movingUI: false,
movingUIState: {},
noShadows: false,
theme: 'Default (Dark) 1.7.1',
@ -160,6 +171,10 @@ let power_user = {
personas: {},
default_persona: null,
persona_descriptions: {},
persona_description: '',
persona_description_position: persona_description_positions.BEFORE_CHAR,
};
let themes = [];
@ -279,9 +294,15 @@ function toggleWaifu() {
}
function switchWaifuMode() {
//console.log(`switching waifu to ${power_user.waifuMode}`);
$("body").toggleClass("waifuMode", power_user.waifuMode);
$("#waifuMode").prop("checked", power_user.waifuMode);
if (isMobile() && !$('body').hasClass('waifuMode')) {
console.debug('saw mobile regular mode, removing ZoomedAvatars');
if ($('.zoomed_avatar[forChar]').length) {
$('.zoomed_avatar[forChar]').remove();
}
return;
}
scrollChatToBottom();
}
@ -575,6 +596,7 @@ function loadPowerUserSettings(settings, data) {
$("#play_message_sound").prop("checked", power_user.play_message_sound);
$("#play_sound_unfocused").prop("checked", power_user.play_sound_unfocused);
$("#never_resize_avatars").prop("checked", power_user.never_resize_avatars);
$("#show_card_avatar_urls").prop("checked", power_user.show_card_avatar_urls);
$("#auto_save_msg_edits").prop("checked", power_user.auto_save_msg_edits);
$("#allow_name1_display").prop("checked", power_user.allow_name1_display);
$("#allow_name2_display").prop("checked", power_user.allow_name2_display);
@ -621,6 +643,31 @@ function loadPowerUserSettings(settings, data) {
loadInstructMode();
loadMaxContextUnlocked();
switchWaifuMode();
loadMovingUIState();
//console.log(power_user)
}
function loadMovingUIState() {
if (isMobile() === false && power_user.movingUIState) {
for (var elmntName of Object.keys(power_user.movingUIState)) {
var elmntState = power_user.movingUIState[elmntName];
try {
var elmnt = $('#' + $.escapeSelector(elmntName));
if (elmnt.length) {
console.debug(`loading state for ${elmntName}`)
elmnt.css(elmntState);
} else {
console.debug(`skipping ${elmntName} because it doesn't exist in the DOM`)
}
} catch (err) {
console.debug(`error occurred while processing ${elmntName}: ${err}`)
}
}
} else {
console.debug('skipping movingUI state load for mobile')
return
}
}
function loadMaxContextUnlocked() {
@ -893,21 +940,26 @@ function resetMovablePanels() {
document.getElementById("right-nav-panel").style.width = '';
document.getElementById("right-nav-panel").style.margin = '';
document.getElementById("expression-holder").style.top = '';
document.getElementById("expression-holder").style.left = '';
document.getElementById("expression-holder").style.right = '';
document.getElementById("expression-holder").style.bottom = '';
document.getElementById("expression-holder").style.height = '';
document.getElementById("expression-holder").style.width = '';
document.getElementById("expression-holder").style.margin = '';
if ($("#expression-holder")) {
document.getElementById("expression-holder").style.top = '';
document.getElementById("expression-holder").style.left = '';
document.getElementById("expression-holder").style.right = '';
document.getElementById("expression-holder").style.bottom = '';
document.getElementById("expression-holder").style.height = '';
document.getElementById("expression-holder").style.width = '';
document.getElementById("expression-holder").style.margin = '';
}
if ($(".zoomed_avatar")) {
$(".zoomed_avatar").css('top', '');
$(".zoomed_avatar").css('left', '');
$(".zoomed_avatar").css('right', '');
$(".zoomed_avatar").css('bottom', '');
$(".zoomed_avatar").css('width', '');
$(".zoomed_avatar").css('height', '');
$(".zoomed_avatar").css('margin', '');
}
document.getElementById("avatar_zoom_popup").style.top = '';
document.getElementById("avatar_zoom_popup").style.left = '';
document.getElementById("avatar_zoom_popup").style.right = '';
document.getElementById("avatar_zoom_popup").style.bottom = '';
document.getElementById("avatar_zoom_popup").style.height = '';
document.getElementById("avatar_zoom_popup").style.width = '';
document.getElementById("avatar_zoom_popup").style.margin = '';
document.getElementById("WorldInfo").style.top = '';
document.getElementById("WorldInfo").style.left = '';
@ -917,7 +969,17 @@ function resetMovablePanels() {
document.getElementById("WorldInfo").style.width = '';
document.getElementById("WorldInfo").style.margin = '';
document.getElementById("floatingPrompt").style.top = '';
document.getElementById("floatingPrompt").style.left = '';
document.getElementById("floatingPrompt").style.right = '';
document.getElementById("floatingPrompt").style.bottom = '';
document.getElementById("floatingPrompt").style.height = '';
document.getElementById("floatingPrompt").style.width = '';
document.getElementById("floatingPrompt").style.margin = '';
$('*[data-dragged="true"]').removeAttr('data-dragged');
power_user.movingUIState = {}
saveSettingsDebounced();
eventSource.emit(event_types.MOVABLE_PANELS_RESET);
}
@ -1159,6 +1221,11 @@ $(document).ready(() => {
power_user.never_resize_avatars = !!$(this).prop('checked');
saveSettingsDebounced();
});
$("#show_card_avatar_urls").on('input', function () {
power_user.show_card_avatar_urls = !!$(this).prop('checked');
printCharacters();
saveSettingsDebounced();
});
$("#play_message_sound").on('input', function () {
power_user.play_message_sound = !!$(this).prop('checked');

2
public/scripts/select2.min.js vendored Normal file

File diff suppressed because one or more lines are too long

View File

@ -457,9 +457,9 @@ export function isDataURL(str) {
return regex.test(str);
}
export function getCharaFilename() {
export function getCharaFilename(chid) {
const context = getContext();
const fileName = context.characters[context.characterId].avatar;
const fileName = context.characters[chid ?? context.characterId].avatar;
if (fileName) {
return fileName.replace(/\.[^/.]+$/, "")

View File

@ -1,8 +1,9 @@
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer } from "./utils.js";
import { saveSettings, callPopup, substituteParams, getTokenCount, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type } from "../script.js";
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, delay, getCharaFilename } from "./utils.js";
import { getContext } from "./extensions.js";
import { metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from "./extensions/floating-prompt/index.js";
import { registerSlashCommand } from "./slash-commands.js";
import { deviceInfo } from "./RossAscends-mods.js";
export {
world_info,
@ -25,7 +26,8 @@ const world_info_insertion_strategy = {
global_first: 2,
};
let world_info = null;
let world_info = {};
let selected_world_info = [];
let world_names;
let world_info_depth = 2;
let world_info_budget = 25;
@ -34,7 +36,10 @@ let world_info_case_sensitive = false;
let world_info_match_whole_words = false;
let world_info_character_strategy = world_info_insertion_strategy.evenly;
const saveWorldDebounced = debounce(async (name, data) => await _save(name, data), 1000);
const saveSettingsDebounced = debounce(() => saveSettings(), 1000);
const saveSettingsDebounced = debounce(() => {
Object.assign(world_info, { globalSelect: selected_world_info })
saveSettings()
}, 1000);
const sortFn = (a, b) => b.order - a.order;
const world_info_position = {
@ -77,6 +82,19 @@ function setWorldInfoSettings(settings, data) {
world_info_budget = 25;
}
// Reset selected world from old string and delete old keys
// TODO: Remove next release
const existingWorldInfo = settings.world_info;
if (typeof existingWorldInfo === "string") {
delete settings.world_info;
selected_world_info = [existingWorldInfo];
} else if (Array.isArray(existingWorldInfo)) {
delete settings.world_info;
selected_world_info = existingWorldInfo;
}
world_info = settings.world_info ?? {}
$("#world_info_depth_counter").text(world_info_depth);
$("#world_info_depth").val(world_info_depth);
@ -92,22 +110,22 @@ function setWorldInfoSettings(settings, data) {
world_names = data.world_names?.length ? data.world_names : [];
if (settings.world_info != undefined) {
if (world_names.includes(settings.world_info)) {
world_info = settings.world_info;
}
// Add to existing selected WI if it exists
selected_world_info = selected_world_info.concat(settings.world_info?.globalSelect?.filter((e) => world_names.includes(e)) ?? []);
if (world_names.length > 0) {
$("#world_info").empty();
}
world_names.forEach((item, i) => {
$("#world_info").append(`<option value='${i}'>${item}</option>`);
$("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
// preselect world if saved
if (item == world_info) {
$("#world_info").val(i).trigger('change');
}
});
$("#world_editor_select").trigger("change");
// Update settings
saveSettingsDebounced();
}
// World Info Editor
@ -160,7 +178,7 @@ async function updateWorldInfoList() {
$("#world_editor_select").find('option[value!=""]').remove();
world_names.forEach((item, i) => {
$("#world_info").append(`<option value='${i}'>${item}</option>`);
$("#world_info").append(`<option value='${i}'${selected_world_info.includes(item) ? ' selected' : ''}>${item}</option>`);
$("#world_editor_select").append(`<option value='${i}'>${item}</option>`);
});
}
@ -170,6 +188,14 @@ function hideWorldEditor() {
displayWorldEntries(null, null);
}
function getWIElement(name) {
const wiElement = $("#world_info").children().filter(function () {
return $(this).text().toLowerCase() === name.toLowerCase()
});
return wiElement;
}
function nullWorldInfo() {
toastr.info("Create or import a new World Info file first.", "World Info is not set", { timeOut: 10000, preventDuplicates: true });
}
@ -224,7 +250,21 @@ function displayWorldEntries(name, data) {
return;
}
await deleteWorldInfo(name, world_info);
const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(name));
if (existingCharLores && existingCharLores.length > 0) {
existingCharLores.forEach((charLore) => {
const tempCharLore = charLore.extraBooks.filter((e) => e !== name);
if (tempCharLore.length === 0) {
world_info.charLore.splice(charLore, 1);
} else {
charLore.extraBooks = tempCharLore;
}
});
saveSettingsDebounced();
}
// Selected world_info automatically refreshes
await deleteWorldInfo(name);
});
// Check if a sortable instance exists
@ -256,7 +296,7 @@ function displayWorldEntries(name, data) {
await saveWorldInfo(name, data, true);
}
});
$("#world_popup_entries_list").disableSelection();
//$("#world_popup_entries_list").disableSelection();
}
function setOriginalDataValue(data, uid, key, value) {
@ -582,19 +622,26 @@ async function renameWorldInfo(name, data) {
return;
}
let selectNewName = null;
if (oldName === world_info) {
console.debug("Renaming current world info");
world_info = newName;
selectNewName = newName;
}
else {
console.debug("Renaming non-current world info");
selectNewName = world_info;
}
const entryPreviouslySelected = selected_world_info.findIndex((e) => e === oldName);
await saveWorldInfo(newName, data, true);
await deleteWorldInfo(oldName, selectNewName);
await deleteWorldInfo(oldName);
const existingCharLores = world_info.charLore?.filter((e) => e.extraBooks.includes(oldName));
if (existingCharLores && existingCharLores.length > 0) {
existingCharLores.forEach((charLore) => {
const tempCharLore = charLore.extraBooks.filter((e) => e !== oldName);
tempCharLore.push(newName);
charLore.extraBooks = tempCharLore;
});
saveSettingsDebounced();
}
if (entryPreviouslySelected !== -1) {
const wiElement = getWIElement(newName);
wiElement.prop("selected", true);
$("#world_info").trigger('change');
}
const selectedIndex = world_names.indexOf(newName);
if (selectedIndex !== -1) {
@ -602,7 +649,7 @@ async function renameWorldInfo(name, data) {
}
}
async function deleteWorldInfo(worldInfoName, selectWorldName) {
async function deleteWorldInfo(worldInfoName) {
if (!world_names.includes(worldInfoName)) {
return;
}
@ -614,16 +661,22 @@ async function deleteWorldInfo(worldInfoName, selectWorldName) {
});
if (response.ok) {
await updateWorldInfoList();
const selectedIndex = world_names.indexOf(selectWorldName);
if (selectedIndex !== -1) {
$("#world_info").val(selectedIndex).trigger('change');
} else {
$("#world_info").val("").trigger('change');
const existingWorldIndex = selected_world_info.findIndex((e) => e === worldInfoName);
if (existingWorldIndex !== -1) {
selected_world_info.splice(existingWorldIndex, 1);
saveSettingsDebounced();
}
await updateWorldInfoList();
$('#world_editor_select').trigger('change');
if ($('#character_world').val() === worldInfoName) {
$('#character_world').val('').trigger('change');
setWorldInfoButtonClass(undefined, false);
if (menu_type != 'create') {
saveCharacterDebounced();
}
}
}
}
@ -663,16 +716,13 @@ async function createNewWorldInfo(worldInfoName) {
return;
}
world_info = worldInfoName;
await saveWorldInfo(world_info, worldInfoTemplate, true);
await saveWorldInfo(worldInfoName, worldInfoTemplate, true);
await updateWorldInfoList();
const selectedIndex = world_names.indexOf(worldInfoName);
if (selectedIndex !== -1) {
$("#world_info").val(selectedIndex).trigger('change');
$('#world_editor_select').val(selectedIndex).trigger('change');
} else {
$("#world_info").val("").trigger('change');
hideWorldEditor();
}
}
@ -683,40 +733,53 @@ function transformString(str) {
}
async function getCharacterLore() {
const name = characters[this_chid]?.data?.extensions?.world;
const character = characters[this_chid];
const name = character?.name;
let worldsToSearch = new Set();
if (!name) {
const baseWorldName = character?.data?.extensions?.world;
if (baseWorldName) {
worldsToSearch.add(baseWorldName);
} else {
console.debug(`Character ${name}'s base world could not be found or is empty! Skipping...`)
return [];
}
if (name === world_info) {
console.debug(`Character ${characters[this_chid]?.name} world info is the same as global: ${name}. Skipping...`);
return [];
// TODO: Maybe make the utility function not use the window context?
const fileName = getCharaFilename(this_chid);
const extraCharLore = world_info.charLore?.find((e) => e.name === fileName);
if (extraCharLore) {
worldsToSearch = new Set([...worldsToSearch, ...extraCharLore.extraBooks]);
}
if (!world_names.includes(name)) {
console.log(`Character ${characters[this_chid]?.name} world info does not exist: ${name}`);
return [];
let entries = [];
for (const worldName of worldsToSearch) {
if (selected_world_info.includes(worldName)) {
console.debug(`Character ${name}'s world ${worldName} is already activated in global world info! Skipping...`);
continue;
}
const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
entries = entries.concat(newEntries);
}
const data = await loadWorldInfoData(name);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
console.debug(`Character ${characters[this_chid]?.name} lore (${name}) has ${entries.length} world info entries`);
console.debug(`Character ${characters[this_chid]?.name} lore (${baseWorldName}) has ${entries.length} world info entries`);
return entries;
}
async function getGlobalLore() {
if (!world_info) {
if (!selected_world_info) {
return [];
}
if (!world_names.includes(world_info)) {
console.log(`Global ${characters[this_chid]?.name} world info does not exist: ${world_info}`);
return [];
let entries = [];
for (const worldName of selected_world_info) {
const data = await loadWorldInfoData(worldName);
const newEntries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
entries = entries.concat(newEntries);
}
const data = await loadWorldInfoData(world_info);
const entries = data ? Object.keys(data.entries).map((x) => data.entries[x]) : [];
console.debug(`Global world info has ${entries.length} entries`);
return entries;
@ -819,11 +882,12 @@ async function checkWorldInfo(chat, maxContext) {
const newEntries = [...activatedNow]
.sort((a, b) => sortedEntries.indexOf(a) - sortedEntries.indexOf(b));
let newContent = "";
const textToScanTokens = getTokenCount(textToScan);
for (const entry of newEntries) {
newContent += `${substituteParams(entry.content)}\n`;
if (getTokenCount(textToScan + newContent) >= budget) {
if (textToScanTokens + getTokenCount(newContent) >= budget) {
console.debug(`WI budget reached, stopping`);
needsToScan = false;
break;
@ -837,28 +901,39 @@ async function checkWorldInfo(chat, maxContext) {
}
}
let worldInfoBefore = "";
let worldInfoAfter = "";
const ANTopInjection = [];
const ANBottomInjection = [];
// Forward-sorted list of entries for joining
const WIBeforeEntries = [];
const WIAfterEntries = [];
const ANTopEntries = [];
const ANBottomEntries = [];
// Appends from insertion order 999 to 1. Use unshift for this purpose
[...allActivatedEntries].sort(sortFn).forEach((entry) => {
if (entry.position === world_info_position.before) {
worldInfoBefore = `${substituteParams(entry.content)}\n${worldInfoBefore}`;
} else if (entry.position === world_info_position.after) {
worldInfoAfter = `${substituteParams(entry.content)}\n${worldInfoAfter}`;
} else if (entry.position === world_info_position.ANTop) {
ANTopInjection.push(entry.content);
} else if (entry.position === world_info_position.ANBottom) {
ANBottomInjection.push(entry.content);
switch (entry.position) {
case world_info_position.before:
WIBeforeEntries.unshift(substituteParams(entry.content));
break;
case world_info_position.after:
WIAfterEntries.unshift(substituteParams(entry.content));
break;
case world_info_position.ANTop:
ANTopEntries.unshift(entry.content);
break;
case world_info_position.ANBottom:
ANBottomEntries.unshift(entry.content);
break;
default:
break;
}
});
const worldInfoBefore = `${WIBeforeEntries.join("\n")}\n`
const worldInfoAfter = `${WIAfterEntries.join("\n")}\n`
if (shouldWIAddPrompt) {
const originalAN = context.extensionPrompts['2_floating_prompt'].value;
const ANWithWI = `\n${ANTopInjection.join("\n")} \n${originalAN} \n${ANBottomInjection.reverse().join("\n")}`
context.setExtensionPrompt('2_floating_prompt', ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
const originalAN = context.extensionPrompts[NOTE_MODULE_NAME].value;
const ANWithWI = `${ANTopEntries.join("\n")}\n${originalAN}\n${ANBottomEntries.join("\n")}`
context.setExtensionPrompt(NOTE_MODULE_NAME, ANWithWI, chat_metadata[metadata_keys.position], chat_metadata[metadata_keys.depth]);
}
return { worldInfoBefore, worldInfoAfter };
@ -993,6 +1068,48 @@ function convertCharacterBook(characterBook) {
return result;
}
export function setWorldInfoButtonClass(chid, forceValue = undefined) {
if (forceValue !== undefined) {
$('#set_character_world, #world_button').toggleClass('world_set', forceValue);
return;
}
if (!chid) {
return;
}
const world = characters[chid]?.data?.extensions?.world;
const worldSet = Boolean(world && world_names.includes(world));
$('#set_character_world, #world_button').toggleClass('world_set', worldSet);
}
export function checkEmbeddedWorld(chid) {
$('#import_character_info').hide();
if (chid === undefined) {
return false;
}
if (characters[chid]?.data?.character_book) {
$('#import_character_info').data('chid', chid).show();
// Only show the alert once per character
const checkKey = `AlertWI_${characters[chid].avatar}`;
const worldName = characters[chid]?.data?.extensions?.world;
if (!localStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
toastr.info(
'To import and use it, select "Import Embedded World Info" in the Options dropdown menu on the character panel.',
`${characters[chid].name} has an embedded World/Lorebook`,
{ timeOut: 10000, extendedTimeOut: 20000, positionClass: 'toast-top-center' },
);
localStorage.setItem(checkKey, 1);
}
return true;
}
return false;
}
export async function importEmbeddedWorldInfo() {
const chid = $('#import_character_info').data('chid');
@ -1021,40 +1138,148 @@ export async function importEmbeddedWorldInfo() {
if (newIndex >= 0) {
$("#world_editor_select").val(newIndex).trigger('change');
}
setWorldInfoButtonClass(chid, true);
}
function onWorldInfoChange(_, text) {
let selectedWorld;
if (_ !== '__notSlashCommand__') { //if it's a slash command
if (text !== undefined) { //and args are provided
let slashInputWorld = text.toLowerCase();
$("#world_info").find(`option`).filter(function () {
return $(this).text().toLowerCase() === slashInputWorld;
}).prop('selected', true); //matches arg with worldnames and selects; if none found, unsets world
let setWorldName = $("#world_info").find(":selected").text(); //only for toastr display
toastr.success(`Active world: ${setWorldName}`);
selectedWorld = $("#world_info").find(":selected").val();
} else { //if no args, unset world
toastr.success('Deselected World')
let selectedWorlds;
if (_ !== '__notSlashCommand__') { // if it's a slash command
if (text !== undefined) { // and args are provided
const slashInputSplitText = text.trim().toLowerCase().split(",");
slashInputSplitText.forEach((worldName) => {
const wiElement = getWIElement(worldName);
if (wiElement.length > 0) {
wiElement.prop("selected", true);
toastr.success(`Activated world: ${wiElement.text()}`);
} else {
toastr.error(`No world found named: ${worldName}`);
}
})
} else { // if no args, unset all worlds
toastr.success('Deactivated all worlds');
selected_world_info = [];
$("#world_info").val("");
}
} else { //if it's a pointer selection
selectedWorld = $("#world_info").find(":selected").val();
}
world_info = null;
if (selectedWorld !== "") {
const worldIndex = Number(selectedWorld);
world_info = !isNaN(worldIndex) ? world_names[worldIndex] : null;
let tempWorldInfo = [];
let selectedWorlds = $("#world_info").val().map((e) => Number(e)).filter((e) => !isNaN(e));
if (selectedWorlds.length > 0) {
selectedWorlds.forEach((worldIndex) => {
const existingWorldName = world_names[worldIndex];
if (existingWorldName) {
tempWorldInfo.push(existingWorldName);
} else {
const wiElement = getWIElement(existingWorldName);
wiElement.prop("selected", false);
toastr.error(`The world with ${existingWorldName} is invalid or corrupted.`);
}
});
}
selected_world_info = tempWorldInfo;
}
saveSettingsDebounced();
}
export async function importWorldInfo(file) {
if (!file) {
return;
}
const formData = new FormData();
formData.append('avatar', file);
try {
let jsonData;
if (file.name.endsWith('.png')) {
const buffer = new Uint8Array(await getFileBuffer(file));
jsonData = extractDataFromPng(buffer, 'naidata');
} else {
// File should be a JSON file
jsonData = await parseJsonFile(file);
}
if (jsonData === undefined || jsonData === null) {
toastr.error(`File is not valid: ${file.name}`);
return;
}
// Convert Novel Lorebook
if (jsonData.lorebookVersion !== undefined) {
console.log('Converting Novel Lorebook');
formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData)));
}
// Convert Agnai Memory Book
if (jsonData.kind === 'memory') {
console.log('Converting Agnai Memory Book');
formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData)));
}
// Convert Risu Lorebook
if (jsonData.type === 'risu') {
console.log('Converting Risu Lorebook');
formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData)));
}
} catch (error) {
toastr.error(`Error parsing file: ${error}`);
return;
}
jQuery.ajax({
type: "POST",
url: "/importworldinfo",
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function (data) {
if (data.name) {
await updateWorldInfoList();
const newIndex = world_names.indexOf(data.name);
if (newIndex >= 0) {
$("#world_editor_select").val(newIndex).trigger('change');
}
toastr.info(`World Info "${data.name}" imported successfully!`);
}
},
error: (jqXHR, exception) => { },
});
}
jQuery(() => {
$(document).ready(function () {
registerSlashCommand('world', onWorldInfoChange, [], " sets active World, or unsets if no args provided", true, true);
})
$("#world_info").on('change', async function () { onWorldInfoChange('__notSlashCommand__') });
let selectScrollTop = null;
$("#world_info").on('mousedown change', async function (e) {
// If there's no world names, don't do anything
if (world_names.length === 0) {
e.preventDefault();
return;
}
if (deviceInfo.device.type === 'desktop') {
e.preventDefault();
const option = $(e.target);
const selectElement = $(this)[0];
selectScrollTop = selectElement.scrollTop;
option.prop('selected', !option.prop('selected'));
await delay(1);
selectElement.scrollTop = selectScrollTop;
}
onWorldInfoChange('__notSlashCommand__');
});
//**************************WORLD INFO IMPORT EXPORT*************************//
$("#world_import_button").on('click', function () {
@ -1064,81 +1289,12 @@ jQuery(() => {
$("#world_import_file").on("change", async function (e) {
const file = e.target.files[0];
if (!file) {
return;
}
const formData = new FormData($("#form_world_import").get(0));
try {
let jsonData;
if (file.name.endsWith('.png')) {
const buffer = new Uint8Array(await getFileBuffer(file));
jsonData = extractDataFromPng(buffer, 'naidata');
} else {
// File should be a JSON file
jsonData = await parseJsonFile(file);
}
if (jsonData === undefined || jsonData === null) {
toastr.error(`File is not valid: ${file.name}`);
return;
}
// Convert Novel Lorebook
if (jsonData.lorebookVersion !== undefined) {
console.log('Converting Novel Lorebook');
formData.append('convertedData', JSON.stringify(convertNovelLorebook(jsonData)));
}
// Convert Agnai Memory Book
if (jsonData.kind === 'memory') {
console.log('Converting Agnai Memory Book');
formData.append('convertedData', JSON.stringify(convertAgnaiMemoryBook(jsonData)));
}
// Convert Risu Lorebook
if (jsonData.type === 'risu') {
console.log('Converting Risu Lorebook');
formData.append('convertedData', JSON.stringify(convertRisuLorebook(jsonData)));
}
} catch (error) {
toastr.error(`Error parsing file: ${error}`);
return;
}
jQuery.ajax({
type: "POST",
url: "/importworldinfo",
data: formData,
beforeSend: () => { },
cache: false,
contentType: false,
processData: false,
success: async function (data) {
if (data.name) {
await updateWorldInfoList();
const newIndex = world_names.indexOf(data.name);
if (newIndex >= 0) {
$("#world_editor_select").val(newIndex).trigger('change');
}
toastr.info(`World Info "${data.name}" imported successfully!`);
}
},
error: (jqXHR, exception) => { },
});
await importWorldInfo(file);
// Will allow to select the same file twice in a row
$("#form_world_import").trigger("reset");
});
$("#world_cross").click(() => {
hideWorldEditor();
});
$("#world_create_button").on('click', async () => {
const tempName = getFreeWorldName();
const finalName = await callPopup("<h3>Create a new World Info?</h3>Enter a name for the new file:", "input", tempName);
@ -1189,5 +1345,27 @@ jQuery(() => {
$('#world_info_character_strategy').on('change', function () {
world_info_character_strategy = $(this).val();
saveSettingsDebounced();
})
});
$('#world_button').on('click', async function () {
const chid = $('#set_character_world').data('chid');
if (chid) {
const worldName = characters[chid]?.data?.extensions?.world;
const hasEmbed = checkEmbeddedWorld(chid);
if (worldName && world_names.includes(worldName)) {
if (!$('#WorldInfo').is(':visible')) {
$('#WIDrawerIcon').trigger('click');
}
const index = world_names.indexOf(worldName);
$("#world_editor_select").val(index).trigger('change');
} else if (hasEmbed) {
await importEmbeddedWorldInfo();
saveCharacterDebounced();
}
else {
$('#char-management-dropdown').val($('#set_character_world').val()).trigger('change');
}
}
});
});

View File

@ -216,6 +216,7 @@ table.responsiveTable {
font-weight: 500;
}
.mes_text strong em,
.mes_text strong,
.mes_text h2,
.mes_text h1 {
@ -702,6 +703,7 @@ hr {
width: 50px;
height: 50px;
border-style: none;
flex: 1;
}
.last_mes .mesAvatarWrapper {
@ -769,6 +771,27 @@ hr {
box-shadow: 0 0 5px var(--black50a);
}
.character_select .avatar,
body.big-avatars .character_select .avatar {
flex: unset;
}
/*
.character_select .avatar img {
flex: unset;
width: 50px;
height: 50px;
width: unset;
aspect-ratio: 1 / 1;
}
,
body.big-avatars .character_select .avatar img {
min-width: 60px;
aspect-ratio: unset;
}
*/
body.no-hotswap .hotswap {
display: none !important;
}
@ -784,13 +807,16 @@ body.no-timestamps .timestamp {
body.big-avatars .avatar {
width: 60px;
height: 90px;
/* width: unset; */
border-style: none;
display: flex;
justify-content: center;
flex-direction: column;
align-items: center;
/* align-self: unset; */
overflow: visible;
border-radius: 10px;
flex: 1
}
body.big-avatars #user_avatar_block .avatar,
@ -806,8 +832,8 @@ body.big-avatars #user_avatar_block .avatar img {
}
body.big-avatars .avatar img {
width: 100%;
height: 100%;
width: 60px;
height: 90px;
object-fit: cover;
object-position: center;
border: 1px solid var(--black30a);
@ -888,7 +914,7 @@ select {
}
#rm_ch_create_block textarea {
min-height: 175px;
min-height: 190px;
}
.margin-bot-10px,
@ -927,7 +953,6 @@ select {
}
#character_cross,
#world_cross,
#select_chat_cross {
position: absolute;
right: 15px;
@ -1012,6 +1037,7 @@ input[type="file"] {
align-items: center;
flex-wrap: wrap;
gap: 10px;
padding: 0 5px;
}
#right-nav-panel-tabs .right_menu_button,
@ -1122,6 +1148,7 @@ input[type="file"] {
text-align: left;
white-space: nowrap;
margin: 0;
font-size: calc(var(--mainFontSize) * 1.25);
}
.selected-right-tab {
@ -1284,13 +1311,14 @@ select option:not(:checked) {
margin: 0;
flex: 1;
border-radius: 7px;
height: auto;
}
#character_search_bar {
margin: 0;
flex: 1;
/* padding-left: 0.75em; */
height: fit-content;
height: auto;
}
input[type=search]::-webkit-search-cancel-button {
@ -1326,6 +1354,10 @@ input[type=search]:focus::-webkit-search-cancel-button {
font-weight: bolder;
}
.ch_avatar_url {
float: right;
}
.character_select .avatar {
align-self: center;
}
@ -1372,6 +1404,10 @@ body.big-avatars .ch_description {
align-items: flex-start !important;
}
.alignSelfStart {
align-self: start;
}
.gap5px {
gap: 5px !important;
}
@ -1491,7 +1527,7 @@ body.big-avatars .ch_description {
height: 100%;
overflow-y: auto;
grid-template-rows:
[avatar] min-content [hr] min-content [search block] min-content [descriptionHeader] min-content [description] auto [firstmessageHeader] min-content [firstMessage] auto [hidden] min-content;
[avatar] min-content [hr] min-content [descriptionHeader] min-content [description] auto [firstmessageHeader] min-content [firstMessage] auto [hidden] min-content;
}
.avatar_div {
@ -1499,10 +1535,21 @@ body.big-avatars .ch_description {
width: 100%;
flex-wrap: wrap;
margin-top: 0px;
/* margin-bottom: 6px; */
align-items: center;
}
/* #avatar_div_div.avatar img {
height: 90%;
width: unset;
aspect-ratio: 1/1;
}
body.big-avatars #avatar_div_div.avatar img {
height: 90%;
width: unset;
aspect-ratio: 2 / 3;
}
*/
#user-settings-block h4,
#user-settings-block h3 {
margin: 5px 0;
@ -1584,7 +1631,7 @@ body.big-avatars .ch_description {
.avatar-container {
position: relative;
display: flex;
flex-direction: column;
flex-direction: row;
align-items: center;
}
@ -1702,6 +1749,7 @@ grammarly-extension {
#result_info {
font-size: calc(var(--mainFontSize) - 0.1rem);
font-weight: bold;
margin-bottom: 5px;
}
/* Focus */
@ -1822,7 +1870,7 @@ grammarly-extension {
font-weight: bold;
padding: 5px;
margin: 0;
height: 35px;
height: 32px;
filter: grayscale(0.5);
text-align: center;
font-size: 20px;
@ -2100,22 +2148,6 @@ grammarly-extension {
display: inline;
}
#world_cross {
position: absolute;
right: 15px;
top: 15px;
width: 20px;
height: 20px;
cursor: pointer;
opacity: 0.6;
}
#world_logo {
width: 35px;
height: 35px;
margin-right: 0.5rem;
}
#world_popup h5 {
color: var(--grey70);
}
@ -2184,13 +2216,6 @@ input[type='checkbox']:not(#nav-toggle):not(#rm_button_panel_pin):not(#lm_button
transform: scale(1);
}
.option_select_right_menu {
width: 284px;
margin-bottom: 35px;
color: var(--white70a);
background-color: var(--black30a);
}
#user_avatar_block {
display: flex;
grid-gap: 10px;
@ -2358,6 +2383,12 @@ input[type="range"]::-webkit-slider-thumb {
color: var(--SmartThemeBodyColor);
}
#char-management-dropdown,
#tagInput {
height: 32px;
margin-bottom: 0;
}
.nice-link:hover {
opacity: 1;
}
@ -2749,12 +2780,6 @@ h5 {
filter: drop-shadow(0 0 2px red);
}
#advanced_book_logo {
width: 35px;
height: 35px;
display: inline-block;
}
#export_character_div {
display: grid;
grid-template-columns: 340px auto;
@ -2806,6 +2831,7 @@ h5 {
display: flex;
flex-direction: row;
align-items: center;
position: relative;
gap: 10px;
width: fit-content;
min-width: 0;
@ -3191,6 +3217,10 @@ body .ui-widget-content li:hover {
background-color: var(--white30a);
}
.group_select .avatar {
flex: 0;
}
.group_select .group_icon {
width: 20px;
height: 20px;
@ -3730,6 +3760,13 @@ label[for="extensions_autoconnect"] {
flex-grow: 1;
}
.menu_button_icon {
display: flex;
align-items: center;
width: fit-content;
gap: 5px;
}
/*------------ TOP SIDE SETTINGS ----------------*/
#top-settings-holder {
@ -3739,10 +3776,10 @@ label[for="extensions_autoconnect"] {
max-width: var(--sheldWidth);
justify-content: center;
display: grid;
grid-template-columns: 10% 10% 10% 10% 10% 10% 10% 10%;
grid-template-columns: 10% 10% 10% 10% 10% 10% 10% 10% 10%;
z-index: 3000;
position: relative;
grid-gap: 2%;
grid-gap: 1%;
}
@ -3820,6 +3857,10 @@ label[for="extensions_autoconnect"] {
flex-basis: calc((var(--sheldWidth) / 4) - 16px);
}
.drawer33pWidth {
flex-basis: calc((var(--sheldWidth) / 3) - 16px);
}
.drawer-content {
background-color: var(--SmartThemeBlurTintColor);
color: var(--SmartThemeBodyColor);
@ -3958,7 +3999,6 @@ toolcool-color-picker {
width: 30% !important;
}
.justifyLeft {
text-align: start;
justify-content: left;
@ -3970,6 +4010,14 @@ toolcool-color-picker {
margin: 0 auto;
}
.justifyContentSpaceAround {
justify-content: space-around;
}
.justifyContentFlexEnd {
justify-content: flex-end;
}
.spaceEvenly {
justify-content: space-evenly;
}
@ -4246,7 +4294,7 @@ body.movingUI .drag-grabber {
body.movingUI #sheld,
body.movingUI .drawer-content,
body.movingUI #expression-holder,
body.movingUI #avatar_zoom_popup,
body.movingUI .zoomed_avatar,
body.movingUI #floatingPrompt {
resize: both;
}
@ -4273,7 +4321,7 @@ body.noShadows * {
color: var(--SmartThemeBodyColor);
}
#avatar_zoom_popup {
.zoomed_avatar {
min-width: 100px;
min-height: 100px;
max-height: 90vh;
@ -4289,7 +4337,7 @@ body.noShadows * {
aspect-ratio: 2 / 3;
}
body.waifuMode #avatar_zoom_popup {
body.waifuMode .zoomed_avatar {
min-width: 100px;
min-height: 100px;
max-height: 90vh;
@ -4308,7 +4356,7 @@ body.waifuMode #avatar_zoom_popup {
aspect-ratio: 2 / 3;
}
#zoomed_avatar {
.zoomed_avatar img {
border: 1px solid var(--white50a);
border-radius: 20px;
height: 100%;
@ -4382,7 +4430,7 @@ body.waifuMode #avatar_zoom_popup {
min-width: 100% !important;
}
#avatar_zoom_popup {
.zoomed_avatar {
min-width: 100px;
min-height: 100px;
max-height: 90vh;
@ -4401,10 +4449,15 @@ body.waifuMode #avatar_zoom_popup {
aspect-ratio: 2 / 3;
}
.world_entry_thin_controls {
.world_entry_thin_controls,
#persona-management-block {
flex-direction: column;
}
#WIMultiSelector {
align-self: normal;
}
.WIEntryContentAndMemo {
flex-flow: column;
}
@ -4573,7 +4626,7 @@ body.waifuMode #avatar_zoom_popup {
}
#rm_ch_create_block textarea {
max-height: 180px;
max-height: 190px;
}
#talkativeness_hint span {
@ -4584,6 +4637,10 @@ body.waifuMode #avatar_zoom_popup {
flex-basis: max(calc(100% / 4 - 10px), 190px);
}
.drawer33pWidth {
flex-basis: max(calc(100% / 3 - 10px), 190px);
}
.expression-holder {
display: none;
}
@ -4614,13 +4671,14 @@ body.waifuMode #avatar_zoom_popup {
object-fit: cover;
}
body:not(.waifuMode) #avatar_zoom_popup {
z-index: 999;
body:not(.waifuMode) .zoomed_avatar {
display: none;
/* z-index: 999;
width: fit-content;
max-height: calc(60svh - 60px);
max-height: calc(60svh - 60px); */
}
body.waifuMode #avatar_zoom_popup {
body.waifuMode .zoomed_avatar {
width: fit-content;
max-height: calc(60svh - 60px);
max-width: 90svw;
@ -4645,14 +4703,33 @@ body.waifuMode #avatar_zoom_popup {
body.waifuMode img.expression {
object-fit: contain;
}
.tag.excluded:after {
top: unset;
bottom: unset;
}
}
@media screen and (max-width: 450px) {
.drawer25pWidth {
flex-basis: max(calc(100% / 2 - 10px), 180px);
}
.drawer33pWidth {
flex-basis: max(calc(100% / 2 - 10px), 180px);
}
.BGSampleTitle {
display: none;
}
.tag.excluded:after {
top: unset;
bottom: unset;
}
}
/*this part only only applies to iOS devices*/
@supports (-webkit-touch-callout: none) {
@ -4725,3 +4802,127 @@ body.waifuMode #avatar_zoom_popup {
height: unset;
}
}
/* Customize the Select2 container */
.select2-container {
color: var(--SmartThemeBodyColor);
}
/* Customize the dropdown */
.select2-dropdown {
background-color: var(--SmartThemeBlurTintColor);
border: 1px solid var(--white30a) !important;
border-radius: 10px;
box-shadow: 0 0 5px black;
text-shadow: 0px 0px calc(var(--shadowWidth) * 1px) var(--SmartThemeShadowColor);
backdrop-filter: blur(calc(var(--SmartThemeBlurStrength)*2));
color: var(--SmartThemeBodyColor);
z-index: 4000;
}
.select2-selection__clear {
color: var(--SmartThemeBodyColor);
}
.select2-container .select2-selection--multiple .select2-selection__choice__remove {
padding: revert;
}
.select2-container .select2-selection--multiple .select2-selection__choice__display {
padding-left: 5px;
}
/* Customize the search input */
.select2-search__field {
background-color: var(--black30a);
color: var(--SmartThemeBodyColor);
border: 1px solid var(--white30a);
border-radius: 7px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 3px 5px;
}
/* Customize the selected option */
.select2-selection--single {
border: 1px solid var(--SmartThemeShadowColor);
border-radius: 4px;
background-color: var(--SmartThemeBlurTintColor);
}
/* Customize the selected option text */
.select2-selection__rendered {
color: var(--SmartThemeBodyColor);
}
/* Customize the option list item */
.select2-results__option {
color: var(--SmartThemeBodyColor);
background-color: var(--SmartThemeBodyColor);
}
.select2-container .select2-selection--multiple {
background-color: var(--black30a);
color: var(--SmartThemeBodyColor);
border: 1px solid var(--white30a);
border-radius: 7px;
font-family: "Noto Sans", "Noto Color Emoji", sans-serif;
padding: 3px 5px;
}
.select2-container .select2-selection--multiple .select2-selection__choice {
border-radius: 5px;
border-style: solid;
border-width: 1px;
box-sizing: border-box;
color: var(--SmartThemeBodyColor);
background-color: var(--black30a);
border-color: var(--white50a);
font-size: calc(var(--mainFontSize) - 5%);
text-shadow: none !important;
}
.select2-results .select2-results__option--selectable {
background-color: unset;
color: var(--SmartThemeBodyColor);
opacity: 0.5;
transition: opacity 200ms ease-in-out;
position: relative;
}
/* Customize the hovered option list item */
.select2-results .select2-results__option--highlighted.select2-results__option--selectable {
color: var(--SmartThemeBodyColor);
background-color: unset;
opacity: 1;
}
/* Customize the option list item */
.select2-results__option {
padding-left: 30px;
/* Add some padding to make room for the checkbox */
}
/* Add the custom checkbox */
.select2-results__option:before {
content: '';
display: inline-block;
position: absolute;
left: 6px;
top: 50%;
margin-top: -7px;
width: 14px;
height: 14px;
border: 1px solid var(--white30a);
background-color: var(--SmartThemeBlurTintColor);
border-radius: 2px;
}
/* Add the custom checkbox checkmark */
.select2-results__option--selected.select2-results__option:before {
content: '\2713';
font-weight: bold;
color: var(--SmartThemeBodyColor);
background-color: var(--SmartThemeBlurTintColor);
text-align: center;
line-height: 14px;
}

347
server.js
View File

@ -128,10 +128,13 @@ let response_getstatus;
const delay = ms => new Promise(resolve => setTimeout(resolve, ms))
const { SentencePieceProcessor, cleanText } = require("sentencepiece-js");
const { Tokenizer } = require('@mlc-ai/web-tokenizers');
const CHARS_PER_TOKEN = 3.35;
let spp_llama;
let spp_nerd;
let spp_nerd_v2;
let claude_tokenizer;
async function loadSentencepieceTokenizer(modelPath) {
try {
@ -147,7 +150,7 @@ async function loadSentencepieceTokenizer(modelPath) {
async function countSentencepieceTokens(spp, text) {
// Fallback to strlen estimation
if (!spp) {
return Math.ceil(text.length / 3.35);
return Math.ceil(text.length / CHARS_PER_TOKEN);
}
let cleaned = cleanText(text);
@ -156,9 +159,36 @@ async function countSentencepieceTokens(spp, text) {
return ids.length;
}
async function loadClaudeTokenizer(modelPath) {
try {
const arrayBuffer = fs.readFileSync(modelPath).buffer;
const instance = await Tokenizer.fromJSON(arrayBuffer);
return instance;
} catch (error) {
console.error("Claude tokenizer failed to load: " + modelPath, error);
return null;
}
}
function countClaudeTokens(tokenizer, messages) {
const convertedPrompt = convertClaudePrompt(messages, false, false);
// Fallback to strlen estimation
if (!tokenizer) {
return Math.ceil(convertedPrompt.length / CHARS_PER_TOKEN);
}
const count = tokenizer.encode(convertedPrompt).length;
return count;
}
const tokenizersCache = {};
function getTokenizerModel(requestModel) {
if (requestModel.includes('claude')) {
return 'claude';
}
if (requestModel.includes('gpt-4-32k')) {
return 'gpt-4-32k';
}
@ -226,6 +256,7 @@ const directories = {
extensions: 'public/scripts/extensions',
instruct: 'public/instruct',
context: 'public/context',
backups: 'backups/',
};
// CSRF Protection //
@ -728,6 +759,12 @@ function convertToV2(char) {
return result;
}
function unsetFavFlag(char) {
const _ = require('lodash');
_.set(char, 'fav', false);
_.set(char, 'data.extensions.fav', false);
}
function readFromV2(char) {
const _ = require('lodash');
if (_.isUndefined(char.data)) {
@ -1049,19 +1086,19 @@ async function charaWrite(img_url, data, target_img, response = undefined, mes =
async function tryReadImage(img_url, crop) {
try {
let rawImg = await jimp.read(img_url);
let final_width = rawImg.bitmap.width, final_height = rawImg.bitmap.height
let final_width = rawImg.bitmap.width, final_height = rawImg.bitmap.height
// Apply crop if defined
if (typeof crop == 'object' && [crop.x, crop.y, crop.width, crop.height].every(x => typeof x === 'number')) {
rawImg = rawImg.crop(crop.x, crop.y, crop.width, crop.height);
// Apply standard resize if requested
if (crop.want_resize) {
final_width = AVATAR_WIDTH
final_height = AVATAR_HEIGHT
}
// Apply standard resize if requested
if (crop.want_resize) {
final_width = AVATAR_WIDTH
final_height = AVATAR_HEIGHT
}
}
const image = await rawImg.cover(final_width, final_height).getBufferAsync(jimp.MIME_PNG);
const image = await rawImg.cover(final_width, final_height).getBufferAsync(jimp.MIME_PNG);
return image;
}
// If it's an unsupported type of image (APNG) - just read the file as buffer
@ -1739,6 +1776,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
if (jsonData.spec !== undefined) {
console.log('importing from v2 json');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
png_name = getPngName(jsonData.data?.name || jsonData.name);
let char = JSON.stringify(jsonData);
@ -1812,6 +1851,8 @@ app.post("/importcharacter", urlencodedParser, async function (request, response
if (jsonData.spec !== undefined) {
console.log('Found a v2 character file.');
importRisuSprites(jsonData);
unsetFavFlag(jsonData);
jsonData = readFromV2(jsonData);
let char = JSON.stringify(jsonData);
charaWrite(uploadPath, char, png_name, response, { file_name: png_name });
@ -2223,7 +2264,7 @@ app.post('/uploaduseravatar', urlencodedParser, async (request, response) => {
const image = await rawImg.cover(AVATAR_WIDTH, AVATAR_HEIGHT).getBufferAsync(jimp.MIME_PNG);
const filename = `${Date.now()}.png`;
const filename = request.body.overwrite_name ?? `${Date.now()}.png`;
const pathToNewFile = path.join(directories.avatars, filename);
fs.writeFileSync(pathToNewFile, image);
fs.rmSync(pathToUpload);
@ -2870,6 +2911,12 @@ app.post("/openai_bias", jsonParser, async function (request, response) {
let result = {};
const model = getTokenizerModel(String(request.query.model || ''));
// no bias for claude
if (model == 'claude') {
return response.send(result);
}
const tokenizer = getTiktokenTokenizer(model);
for (const entry of request.body) {
@ -2942,7 +2989,7 @@ app.post("/deletepreset_openai", jsonParser, function (request, response) {
});
// Prompt Conversion script taken from RisuAI by @kwaroran (GPLv3).
function convertClaudePrompt(messages) {
function convertClaudePrompt(messages, addHumanPrefix, addAssistantPostfix) {
// Claude doesn't support message names, so we'll just add them to the message content.
for (const message of messages) {
if (message.name && message.role !== "system") {
@ -2972,7 +3019,16 @@ function convertClaudePrompt(messages) {
break
}
return prefix + v.content;
}).join('') + '\n\nAssistant: ';
}).join('');
if (addHumanPrefix) {
requestPrompt = "\n\nHuman: " + requestPrompt;
}
if (addAssistantPostfix) {
requestPrompt = requestPrompt + '\n\nAssistant: ';
}
return requestPrompt;
}
@ -2993,14 +3049,14 @@ async function sendClaudeRequest(request, response) {
controller.abort();
});
const requestPrompt = convertClaudePrompt(request.body.messages);
const requestPrompt = convertClaudePrompt(request.body.messages, true, true);
console.log('Claude request:', requestPrompt);
const generateResponse = await fetch(api_url + '/complete', {
method: "POST",
signal: controller.signal,
body: JSON.stringify({
prompt: "\n\nHuman: " + requestPrompt,
prompt: requestPrompt,
model: request.body.model,
max_tokens_to_sample: request.body.max_tokens,
stop_sequences: ["\n\nHuman:", "\n\nSystem:", "\n\nAssistant:"],
@ -3166,15 +3222,20 @@ app.post("/generate_openai", jsonParser, function (request, response_generate_op
app.post("/tokenize_openai", jsonParser, function (request, response_tokenize_openai = response) {
if (!request.body) return response_tokenize_openai.sendStatus(400);
let num_tokens = 0;
const model = getTokenizerModel(String(request.query.model || ''));
if (model == 'claude') {
num_tokens = countClaudeTokens(claude_tokenizer, request.body);
return response_tokenize_openai.send({ "token_count": num_tokens });
}
const tokensPerName = model.includes('gpt-4') ? 1 : -1;
const tokensPerMessage = model.includes('gpt-4') ? 3 : 4;
const tokensPadding = 3;
const tokenizer = getTiktokenTokenizer(model);
let num_tokens = 0;
for (const msg of request.body) {
num_tokens += tokensPerMessage;
for (const [key, value] of Object.entries(msg)) {
@ -3275,6 +3336,7 @@ const setupTasks = async function () {
console.log(`SillyTavern ${version.pkgVersion}` + (version.gitBranch ? ` '${version.gitBranch}' (${version.gitRevision})` : ''));
backupSettings();
migrateSecrets();
ensurePublicDirectoriesExist();
await ensureThumbnailCache();
@ -3282,10 +3344,11 @@ const setupTasks = async function () {
// Colab users could run the embedded tool
if (!is_colab) await convertWebp();
[spp_llama, spp_nerd, spp_nerd_v2] = await Promise.all([
[spp_llama, spp_nerd, spp_nerd_v2, claude_tokenizer] = await Promise.all([
loadSentencepieceTokenizer('src/sentencepiece/tokenizer.model'),
loadSentencepieceTokenizer('src/sentencepiece/nerdstash.model'),
loadSentencepieceTokenizer('src/sentencepiece/nerdstash_v2.model'),
loadClaudeTokenizer('src/claude.json'),
]);
console.log('Launching...');
@ -3365,6 +3428,41 @@ async function convertWebp() {
}
}
function backupSettings() {
const MAX_BACKUPS = 25;
function generateTimestamp() {
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
return `${year}${month}${day}-${hours}${minutes}${seconds}`;
}
try {
if (!fs.existsSync(directories.backups)) {
fs.mkdirSync(directories.backups);
}
const backupFile = path.join(directories.backups, `settings_${generateTimestamp()}.json`);
fs.copyFileSync(SETTINGS_FILE, backupFile);
let files = fs.readdirSync(directories.backups);
if (files.length > MAX_BACKUPS) {
files = files.map(f => path.join(directories.backups, f));
files.sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
fs.rmSync(files[0]);
}
} catch (err) {
console.log('Could not backup settings file', err);
}
}
function ensurePublicDirectoriesExist() {
for (const dir of Object.values(directories)) {
if (!fs.existsSync(dir)) {
@ -3381,6 +3479,7 @@ const SECRET_KEYS = {
POE: 'api_key_poe',
NOVEL: 'api_key_novel',
CLAUDE: 'api_key_claude',
DEEPL: 'deepl',
}
function migrateSecrets() {
@ -3631,6 +3730,53 @@ app.post('/google_translate', jsonParser, async (request, response) => {
});
});
app.post('/deepl_translate', jsonParser, async (request, response) => {
const key = readSecret(SECRET_KEYS.DEEPL);
if (!key) {
return response.sendStatus(401);
}
const text = request.body.text;
const lang = request.body.lang;
if (!text || !lang) {
return response.sendStatus(400);
}
console.log('Input text: ' + text);
const fetch = require('node-fetch').default;
const params = new URLSearchParams();
params.append('text', text);
params.append('target_lang', lang);
try {
const result = await fetch('https://api-free.deepl.com/v2/translate', {
method: 'POST',
body: params,
headers: {
'Accept': 'application/json',
'Authorization': `DeepL-Auth-Key ${key}`,
'Content-Type': 'application/x-www-form-urlencoded',
},
timeout: 0,
});
if (!result.ok) {
return response.sendStatus(result.status);
}
const json = await result.json();
console.log('Translated text: ' + json.translations[0].text);
return response.send(json.translations[0].text);
} catch (error) {
console.log("Translation error: " + error.message);
return response.sendStatus(500);
}
});
app.post('/novel_tts', jsonParser, async (request, response) => {
const token = readSecret(SECRET_KEYS.NOVEL);
@ -3796,6 +3942,177 @@ app.post('/upload_sprite', urlencodedParser, async (request, response) => {
}
});
app.post('/import_custom', jsonParser, async (request, response) => {
if (!request.body.url) {
return response.sendStatus(400);
}
try {
const url = request.body.url;
let result;
const chubParsed = parseChubUrl(url);
if (chubParsed?.type === 'character') {
console.log('Downloading chub character:', chubParsed.id);
result = await downloadChubCharacter(chubParsed.id);
}
else if (chubParsed?.type === 'lorebook') {
console.log('Downloading chub lorebook:', chubParsed.id);
result = await downloadChubLorebook(chubParsed.id);
}
else {
return response.sendStatus(404);
}
response.set('Content-Type', result.fileType);
response.set('Content-Disposition', `attachment; filename="${result.fileName}"`);
response.set('X-Custom-Content-Type', chubParsed?.type);
return response.send(result.buffer);
} catch (error) {
console.log('Importing custom content failed', error);
return response.sendStatus(500);
}
});
async function downloadChubLorebook(id) {
const fetch = require('node-fetch').default;
const result = await fetch('https://api.chub.ai/api/lorebooks/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"fullPath": id,
"format": "SILLYTAVERN",
}),
});
if (!result.ok) {
throw new Error('Failed to download lorebook');
}
const name = id.split('/').pop();
const buffer = await result.buffer();
const fileName = `${sanitize(name)}.json`;
const fileType = result.headers.get('content-type');
return { buffer, fileName, fileType };
}
async function downloadChubCharacter(id) {
const fetch = require('node-fetch').default;
const result = await fetch('https://api.chub.ai/api/characters/download', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
"format": "tavern",
"fullPath": id,
})
});
if (!result.ok) {
throw new Error('Failed to download character');
}
const buffer = await result.buffer();
const fileName = result.headers.get('content-disposition').split('filename=')[1];
const fileType = result.headers.get('content-type');
return { buffer, fileName, fileType };
}
function parseChubUrl(str) {
const splitStr = str.split('/');
const length = splitStr.length;
if (length < 2) {
return null;
}
const domainIndex = splitStr.indexOf('chub.ai');
const lastTwo = domainIndex !== -1 ? splitStr.slice(domainIndex + 1) : splitStr;
const firstPart = lastTwo[0].toLowerCase();
if (firstPart === 'characters' || firstPart === 'lorebooks') {
const type = firstPart === 'characters' ? 'character' : 'lorebook';
const id = type === 'character' ? lastTwo.slice(1).join('/') : lastTwo.join('/');
return {
id: id,
type: type
};
} else if (length === 2) {
return {
id: lastTwo.join('/'),
type: 'character'
};
}
return null;
}
function importRisuSprites(data) {
try {
const name = data?.data?.name;
const risuData = data?.data?.extensions?.risuai;
// Not a Risu AI character
if (!risuData || !name) {
return;
}
let images = [];
if (Array.isArray(risuData.additionalAssets)) {
images = images.concat(risuData.additionalAssets);
}
if (Array.isArray(risuData.emotions)) {
images = images.concat(risuData.emotions);
}
// No sprites to import
if (images.length === 0) {
return;
}
// Create sprites folder if it doesn't exist
const spritesPath = path.join(directories.characters, name);
if (!fs.existsSync(spritesPath)) {
fs.mkdirSync(spritesPath);
}
// Path to sprites is not a directory. This should never happen.
if (!fs.statSync(spritesPath).isDirectory()) {
return;
}
console.log(`RisuAI: Found ${images.length} sprites for ${name}. Writing to disk.`);
const files = fs.readdirSync(spritesPath);
outer: for (const [label, fileBase64] of images) {
// Remove existing sprite with the same label
for (const file of files) {
if (path.parse(file).name === label) {
console.log(`RisuAI: The sprite ${label} for ${name} already exists. Skipping.`);
continue outer;
}
}
const filename = label + '.png';
const pathToFile = path.join(spritesPath, filename);
fs.writeFileSync(pathToFile, fileBase64, { encoding: 'base64' });
}
// Remove additionalAssets and emotions from data (they are now in the sprites folder)
delete data.data.extensions.risuai.additionalAssets;
delete data.data.extensions.risuai.emotions;
} catch (error) {
console.error(error);
}
}
function writeSecret(key, value) {
if (!fs.existsSync(SECRETS_FILE)) {
const emptyFile = JSON.stringify({});

1
src/claude.json Normal file

File diff suppressed because one or more lines are too long