Merge pull request #3463 from SillyTavern/bogus-localstorage Add settings.json-backed KV string storage

43f524bb5f4ffebfd3bf5140e50f5fc8f34051f9

Cohee <18619528+Cohee1207@users.noreply.github.com>

Signed
18 files changed, +251 -108Ignore whitespace
public/script.js+17 -10
@@ -270,6 +270,7 @@ import { initBulkEdit } from './scripts/bulk-edit.js';
270270import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
271271import { getContext } from './scripts/st-context.js';
272272import { extractReasoningFromData, initReasoning, PromptReasoning, removeReasoningFromString, updateReasoningTimeUI } from './scripts/reasoning.js';
273+import { accountStorage } from './scripts/util/AccountStorage.js';
273274
274275// API OBJECT FOR EXTERNAL WIRING
275276globalThis.SillyTavern = {
@@ -419,7 +420,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
419420 const entityId = getCurrentEntityId();
420421 const warningShownKey = `mediaWarningShown:${entityId}`;
421422
422423 if (localStorageaccountStorage.getItem(warningShownKey) === null) {
423424 const warningToast = toastr.warning(
424425 t`Use the 'Ext. Media' button to allow it. Click on this message to dismiss.`,
425426 t`External media has been blocked`,
@@ -430,7 +431,7 @@ DOMPurify.addHook('uponSanitizeElement', (node, _, config) => {
430431 },
431432 );
432433
433434 localStorageaccountStorage.setItem(warningShownKey, 'true');
434435 }
435436 }
436437});
@@ -1490,7 +1491,7 @@ export async function printCharacters(fullRefresh = false) {
14901491
14911492 $('#rm_print_characters_pagination').pagination({
14921493 dataSource: entities,
14931494 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || per_page_default,
14941495 sizeChangerOptions: [10, 25, 50, 100, 250, 500, 1000],
14951496 pageRange: 1,
14961497 pageNumber: saveCharactersPage || 1,
@@ -1534,7 +1535,7 @@ export async function printCharacters(fullRefresh = false) {
15341535 eventSource.emit(event_types.CHARACTER_PAGE_LOADED);
15351536 },
15361537 afterSizeSelectorChange: function (e) {
15371538 localStorageaccountStorage.setItem(storageKey, e.target.value);
15381539 },
15391540 afterPaging: function (e) {
15401541 saveCharactersPage = e;
@@ -6903,10 +6904,11 @@ export async function getSettings() {
69036904 $('#your_name').val(name1);
69046905 }
69056906
6907+ accountStorage.init(settings?.accountStorage);
69066908 await setUserControls(data.enable_accounts);
69076909
69086910 // Allow subscribers to mutate settings
69096911 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
69106912
69116913 //Load KoboldAI settings
69126914 koboldai_setting_names = data.koboldai_setting_names;
@@ -7074,7 +7076,8 @@ function selectKoboldGuiPreset() {
70747076
70757077export async function saveSettings(loopCounter = 0) {
70767078 if (!settingsReady) {
70777079 console.warn('Settings not ready, abortingscheduling another save');
7080+ saveSettingsDebounced();
70787081 return;
70797082 }
70807083
@@ -7095,6 +7098,7 @@ export async function saveSettings(loopCounter = 0) {
70957098 url: '/api/settings/save',
70967099 data: JSON.stringify({
70977100 firstRun: firstRun,
7101+ accountStorage: accountStorage.getState(),
70987102 currentVersion: currentVersion,
70997103 username: name1,
71007104 active_character: active_character,
@@ -7550,7 +7554,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
75507554 }
75517555
75527556 try {
75537557 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
75547558 const page = Math.floor(charIndex / perPage) + 1;
75557559 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
75567560 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7582,7 +7586,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
75827586 return;
75837587 }
75847588
75857589 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
75867590 const page = Math.floor(charIndex / perPage) + 1;
75877591 $('#rm_print_characters_pagination').pagination('go', page);
75887592 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -9449,6 +9453,9 @@ export async function deleteCharacter(characterKey, { deleteChats = true } = {})
94499453 continue;
94509454 }
94519455
9456+ accountStorage.removeItem(`AlertWI_${character.avatar}`);
9457+ accountStorage.removeItem(`AlertRegex_${character.avatar}`);
9458+ accountStorage.removeItem(`mediaWarningShown:${character.avatar}`);
94529459 delete tag_map[character.avatar];
94539460 select_rm_info('char_delete', character.name);
94549461
@@ -9651,8 +9658,8 @@ function addDebugFunctions() {
96519658 });
96529659
96539660 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
96549661 localStorageaccountStorage.setItem('RegenerateWithCtrlEnter', localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
96559662 toastr.info('Regenerate warning is now ' + (localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
96569663 });
96579664
96589665 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
public/scripts/RossAscends-mods.js+29 -25
@@ -27,7 +27,6 @@ import {
2727 send_on_enter_options,
2828} from './power-user.js';
2929
30-import { LoadLocal, SaveLocal, LoadLocalBool } from './f-localStorage.js';
3130import { selected_group, is_group_generating, openGroupById } from './group-chats.js';
3231import { getTagKeyForEntity, applyTagsOnCharacterSelect } from './tags.js';
3332import {
@@ -41,6 +40,8 @@ import { textgen_types, textgenerationwebui_settings as textgen_settings, getTex
4140import { debounce_timeout } from './constants.js';
4241
4342import { Popup } from './popup.js';
43+import { accountStorage } from './util/AccountStorage.js';
44+import { getCurrentUserHandle } from './user.js';
4445
4546var RPanelPin = document.getElementById('rm_button_panel_pin');
4647var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -409,32 +410,34 @@ function RA_autoconnect(PrevApi) {
409410function OpenNavPanels() {
410411 if (!isMobile()) {
411412 //auto-open R nav if locked and previously open
412413 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('NavOpened') == 'true') {
413414 //console.log("RA -- clicking right nav to open");
414415 $('#rightNavDrawerIcon').click();
415416 }
416417
417418 //auto-open L nav if locked and previously open
418419 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('LNavOpened') == 'true') {
419420 console.debug('RA -- clicking left nav to open');
420421 $('#leftNavDrawerIcon').click();
421422 }
422423
423424 //auto-open WI if locked and previously open
424425 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('WINavOpened') == 'true') {
425426 console.debug('RA -- clicking WI to open');
426427 $('#WIDrawerIcon').click();
427428 }
428429 }
429430}
430431
432+const getUserInputKey = () => getCurrentUserHandle() + '_userInput';
433+
431434function restoreUserInput() {
432435 if (!power_user.restore_user_input) {
433436 console.debug('restoreUserInput disabled');
434437 return;
435438 }
436439
437440 const userInput = LoadLocallocalStorage.getItem('userInput'getUserInputKey());
438441 if (userInput) {
439442 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440443 }
@@ -442,7 +445,8 @@ function restoreUserInput() {
442445
443446function saveUserInput() {
444447 const userInput = String($('#send_textarea').val());
445448 SaveLocallocalStorage.setItem('userInput'getUserInputKey(), userInput);
449+ console.debug('User Input -- ', userInput);
446450}
447451const saveUserInputDebounced = debounce(saveUserInput);
448452
@@ -739,7 +743,7 @@ export function initRossMods() {
739743
740744 //toggle pin class when lock toggle clicked
741745 $(RPanelPin).on('click', function () {
742746 SaveLocalaccountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743747 if ($(RPanelPin).prop('checked') == true) {
744748 //console.log('adding pin class to right nav');
745749 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +761,7 @@ export function initRossMods() {
757761 }
758762 });
759763 $(LPanelPin).on('click', function () {
760764 SaveLocalaccountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761765 if ($(LPanelPin).prop('checked') == true) {
762766 //console.log('adding pin class to Left nav');
763767 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +780,7 @@ export function initRossMods() {
776780 });
777781
778782 $(WIPanelPin).on('click', function () {
779783 SaveLocalaccountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780784 if ($(WIPanelPin).prop('checked') == true) {
781785 console.debug('adding pin class to WI');
782786 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +800,8 @@ export function initRossMods() {
796800 });
797801
798802 // read the state of right Nav Lock and apply to rightnav classlist
799803 $(RPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true');
800804 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true') {
801805 //console.log('setting pin class via local var');
802806 $(RightNavPanel).addClass('pinnedOpen');
803807 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +812,8 @@ export function initRossMods() {
808812 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809813 }
810814 // read the state of left Nav Lock and apply to leftnav classlist
811815 $(LPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('LNavLockOn') === 'true');
812816 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true') {
813817 //console.log('setting pin class via local var');
814818 $(LeftNavPanel).addClass('pinnedOpen');
815819 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +825,8 @@ export function initRossMods() {
821825 }
822826
823827 // read the state of left Nav Lock and apply to leftnav classlist
824828 $(WIPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('WINavLockOn') === 'true');
825829 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true') {
826830 //console.log('setting pin class via local var');
827831 $(WorldInfo).addClass('pinnedOpen');
828832 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +841,22 @@ export function initRossMods() {
837841 //save state of Right nav being open or closed
838842 $('#rightNavDrawerIcon').on('click', function () {
839843 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840844 SaveLocalaccountStorage.setItem('NavOpened', 'true');
841845 } else { SaveLocalaccountStorage.setItem('NavOpened', 'false'); }
842846 });
843847
844848 //save state of Left nav being open or closed
845849 $('#leftNavDrawerIcon').on('click', function () {
846850 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847851 SaveLocalaccountStorage.setItem('LNavOpened', 'true');
848852 } else { SaveLocalaccountStorage.setItem('LNavOpened', 'false'); }
849853 });
850854
851855 //save state of Left nav being open or closed
852856 $('#WorldInfo').on('click', function () {
853857 if (!$('#WorldInfo').hasClass('openIcon')) {
854858 SaveLocalaccountStorage.setItem('WINavOpened', 'true');
855859 } else { SaveLocalaccountStorage.setItem('WINavOpened', 'false'); }
856860 });
857861
858862 var chatbarInFocus = false;
@@ -868,8 +872,8 @@ export function initRossMods() {
868872 OpenNavPanels();
869873 }, 300);
870874
871875 $(SelectedCharacterTab).click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872876 $('#rm_button_characters').click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873877
874878 // when a char is selected from the list, save them as the auto-load character for next page load
875879
@@ -1077,7 +1081,7 @@ export function initRossMods() {
10771081 }
10781082 else if (is_send_press == false) {
10791083 const skipConfirmKey = 'RegenerateWithCtrlEnter';
10801084 const skipConfirm = LoadLocalBoolaccountStorage.getItem(skipConfirmKey) === 'true';
10811085 function doRegenerate() {
10821086 console.debug('Regenerating with Ctrl+Enter');
10831087 $('#option_regenerate').trigger('click');
@@ -1097,7 +1101,7 @@ export function initRossMods() {
10971101 return;
10981102 }
10991103
11001104 SaveLocalaccountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
11011105 doRegenerate();
11021106 }
11031107 return;
public/scripts/chats.js+5 -4
@@ -45,6 +45,7 @@ import { DragAndDropHandler } from './dragdrop.js';
4545import { renderTemplateAsync } from './templates.js';
4646import { t } from './i18n.js';
4747import { humanizedDateTime } from './RossAscends-mods.js';
48+import { accountStorage } from './util/AccountStorage.js';
4849
4950/**
5051 * @typedef {Object} FileAttachment
@@ -1078,8 +1079,8 @@ async function openAttachmentManager() {
10781079 renderAttachments();
10791080 });
10801081
10811082 let sortField = localStorageaccountStorage.getItem('DataBank_sortField') || 'created';
10821083 let sortOrder = localStorageaccountStorage.getItem('DataBank_sortOrder') || 'desc';
10831084 let filterString = '';
10841085
10851086 const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
@@ -1095,8 +1096,8 @@ async function openAttachmentManager() {
10951096
10961097 sortField = this.selectedOptions[0].dataset.sortField;
10971098 sortOrder = this.selectedOptions[0].dataset.sortOrder;
10981099 localStorageaccountStorage.setItem('DataBank_sortField', sortField);
10991100 localStorageaccountStorage.setItem('DataBank_sortOrder', sortOrder);
11001101 renderAttachments();
11011102 });
11021103 function handleBulkAction(action) {
public/scripts/extensions.js+5 -4
@@ -9,6 +9,7 @@ import { getContext } from './st-context.js';
99import { isAdmin } from './user.js';
1010import { t } from './i18n.js';
1111import { debounce_timeout } from './constants.js';
12+import { accountStorage } from './util/AccountStorage.js';
1213
1314export {
1415 getContext,
@@ -714,7 +715,7 @@ async function showExtensionsDetails() {
714715 htmlExternal.append(htmlLoading);
715716
716717 const sortOrderKey = 'extensions_sortByName';
717718 const sortByName = localStorageaccountStorage.getItem(sortOrderKey) === 'true';
718719 const sortFn = sortByName ? sortManifestsByName : sortManifestsByOrder;
719720 const extensions = Object.entries(manifests).sort((a, b) => sortFn(a[1], b[1])).map(getExtensionData);
720721
@@ -745,7 +746,7 @@ async function showExtensionsDetails() {
745746 text: sortByName ? t`Sort: Display Name` : t`Sort: Loading Order`,
746747 action: async () => {
747748 abortController.abort();
748749 localStorageaccountStorage.setItem(sortOrderKey, sortByName ? 'false' : 'true');
749750 await showExtensionsDetails();
750751 },
751752 };
@@ -1153,11 +1154,11 @@ async function checkForExtensionUpdates(force) {
11531154 const currentDate = new Date().toDateString();
11541155
11551156 // Don't nag more than once a day
11561157 if (localStorageaccountStorage.getItem(STORAGE_NAG_KEY) === currentDate) {
11571158 return;
11581159 }
11591160
11601161 localStorageaccountStorage.setItem(STORAGE_NAG_KEY, currentDate);
11611162 }
11621163
11631164 const isCurrentUserAdmin = isAdmin();
public/scripts/extensions/assets/index.js+3 -2
@@ -8,6 +8,7 @@ import { getRequestHeaders, processDroppedFiles, eventSource, event_types } from
88import { deleteExtension, extensionNames, getContext, installExtension, renderExtensionTemplateAsync } from '../../extensions.js';
99import { POPUP_TYPE, Popup, callGenericPopup } from '../../popup.js';
1010import { executeSlashCommands } from '../../slash-commands.js';
11+import { accountStorage } from '../../util/AccountStorage.js';
1112import { flashHighlight, getStringHash, isValidUrl } from '../../utils.js';
1213export { MODULE_NAME };
1314
@@ -432,14 +433,14 @@ jQuery(async () => {
432433 connectButton.on('click', async function () {
433434 const url = DOMPurify.sanitize(String(assetsJsonUrl.val()));
434435 const rememberKey = `Assets_SkipConfirm_${getStringHash(url)}`;
435436 const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true';
436437
437438 const confirmation = skipConfirm || await Popup.show.confirm('Loading Asset List', `<span>Are you sure you want to connect to the following url?</span><var>${url}</var>`, {
438439 customInputs: [{ id: 'assets-remember', label: 'Don\'t ask again for this URL' }],
439440 onClose: popup => {
440441 if (popup.result) {
441442 const rememberValue = popup.inputResults.get('assets-remember');
442443 localStorageaccountStorage.setItem(rememberKey, String(rememberValue));
443444 }
444445 },
445446 });
public/scripts/extensions/quick-reply/src/QuickReply.js+9 -8
@@ -10,6 +10,7 @@ import { SlashCommandExecutor } from '../../../slash-commands/SlashCommandExecut
1010import { SlashCommandParser } from '../../../slash-commands/SlashCommandParser.js';
1111import { SlashCommandParserError } from '../../../slash-commands/SlashCommandParserError.js';
1212import { SlashCommandScope } from '../../../slash-commands/SlashCommandScope.js';
13+import { accountStorage } from '../../../util/AccountStorage.js';
1314import { debounce, delay, getSortableDelay, showFontAwesomePicker } from '../../../utils.js';
1415import { log, quickReplyApi, warn } from '../index.js';
1516import { QuickReplyContextLink } from './QuickReplyContextLink.js';
@@ -544,9 +545,9 @@ export class QuickReply {
544545 this.editorSyntax = messageSyntaxInner;
545546 /**@type {HTMLInputElement}*/
546547 const wrap = dom.querySelector('#qr--modal-wrap');
547548 wrap.checked = JSON.parse(localStorageaccountStorage.getItem('qr--wrap') ?? 'false');
548549 wrap.addEventListener('click', () => {
549550 localStorageaccountStorage.setItem('qr--wrap', JSON.stringify(wrap.checked));
550551 updateWrap();
551552 });
552553 const updateWrap = () => {
@@ -594,27 +595,27 @@ export class QuickReply {
594595 };
595596 /**@type {HTMLInputElement}*/
596597 const tabSize = dom.querySelector('#qr--modal-tabSize');
597598 tabSize.value = JSON.parse(localStorageaccountStorage.getItem('qr--tabSize') ?? '4');
598599 const updateTabSize = () => {
599600 message.style.tabSize = tabSize.value;
600601 messageSyntaxInner.style.tabSize = tabSize.value;
601602 updateScrollDebounced();
602603 };
603604 tabSize.addEventListener('change', () => {
604605 localStorageaccountStorage.setItem('qr--tabSize', JSON.stringify(Number(tabSize.value)));
605606 updateTabSize();
606607 });
607608 /**@type {HTMLInputElement}*/
608609 const executeShortcut = dom.querySelector('#qr--modal-executeShortcut');
609610 executeShortcut.checked = JSON.parse(localStorageaccountStorage.getItem('qr--executeShortcut') ?? 'true');
610611 executeShortcut.addEventListener('click', () => {
611612 localStorageaccountStorage.setItem('qr--executeShortcut', JSON.stringify(executeShortcut.checked));
612613 });
613614 /**@type {HTMLInputElement}*/
614615 const syntax = dom.querySelector('#qr--modal-syntax');
615616 syntax.checked = JSON.parse(localStorageaccountStorage.getItem('qr--syntax') ?? 'true');
616617 syntax.addEventListener('click', () => {
617618 localStorageaccountStorage.setItem('qr--syntax', JSON.stringify(syntax.checked));
618619 updateSyntaxEnabled();
619620 });
620621 if (navigator.keyboard) {
public/scripts/extensions/regex/index.js+3 -2
@@ -10,6 +10,7 @@ import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
1010import { download, getFileText, getSortableDelay, uuidv4 } from '../../utils.js';
1111import { regex_placement, runRegexScript, substitute_find_regex } from './engine.js';
1212import { t } from '../../i18n.js';
13+import { accountStorage } from '../../util/AccountStorage.js';
1314
1415/**
1516 * @typedef {object} RegexScript
@@ -440,8 +441,8 @@ async function checkEmbeddedRegexScripts() {
440441 if (avatar && !extension_settings.character_allowed_regex.includes(avatar)) {
441442 const checkKey = `AlertRegex_${characters[chid].avatar}`;
442443
443444 if (!localStorageaccountStorage.getItem(checkKey)) {
444445 localStorageaccountStorage.setItem(checkKey, 'true');
445446 const template = await renderExtensionTemplateAsync('regex', 'embeddedScripts', {});
446447 const result = await callGenericPopup(template, POPUP_TYPE.CONFIRM, '', { okButton: 'Yes' });
447448
public/scripts/f-localStorage.js+0 -27
@@ -1,27 +0,0 @@
1-////////////////// LOCAL STORAGE HANDLING /////////////////////
2-
3-export function SaveLocal(target, val) {
4- localStorage.setItem(target, val);
5- console.debug('SaveLocal -- ' + target + ' : ' + val);
6-}
7-export function LoadLocal(target) {
8- console.debug('LoadLocal -- ' + target);
9- return localStorage.getItem(target);
10-
11-}
12-export function LoadLocalBool(target) {
13- let result = localStorage.getItem(target) === 'true';
14- return result;
15-}
16-export function CheckLocal() {
17- console.log('----------local storage---------');
18- var i;
19- for (i = 0; i < localStorage.length; i++) {
20- console.log(localStorage.key(i) + ' : ' + localStorage.getItem(localStorage.key(i)));
21- }
22- console.log('------------------------------');
23-}
24-
25-export function ClearLocal() { localStorage.clear(); console.log('Removed All Local Storage'); }
26-
27-/////////////////////////////////////////////////////////////////////////
public/scripts/group-chats.js+5 -4
@@ -78,6 +78,7 @@ import { FILTER_TYPES, FilterHelper } from './filters.js';
7878import { isExternalMediaAllowed } from './chats.js';
7979import { POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
8080import { t } from './i18n.js';
81+import { accountStorage } from './util/AccountStorage.js';
8182
8283export {
8384 selected_group,
@@ -1309,10 +1310,10 @@ function printGroupCandidates() {
13091310 formatNavigator: PAGINATION_TEMPLATE,
13101311 showNavigator: true,
13111312 showSizeChanger: true,
13121313 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13131314 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13141315 afterSizeSelectorChange: function (e) {
13151316 localStorageaccountStorage.setItem(storageKey, e.target.value);
13161317 },
13171318 callback: function (data) {
13181319 $('#rm_group_add_members').empty();
@@ -1336,10 +1337,10 @@ function printGroupMembers() {
13361337 formatNavigator: PAGINATION_TEMPLATE,
13371338 showNavigator: true,
13381339 showSizeChanger: true,
13391340 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || 5,
13401341 sizeChangerOptions: [5, 10, 25, 50, 100, 200, 500, 1000],
13411342 afterSizeSelectorChange: function (e) {
13421343 localStorageaccountStorage.setItem(storageKey, e.target.value);
13431344 },
13441345 callback: function (data) {
13451346 $('.rm_group_members').empty();
public/scripts/openai.js+3 -2
@@ -73,6 +73,7 @@ import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js
7373import { Popup, POPUP_RESULT } from './popup.js';
7474import { t } from './i18n.js';
7575import { ToolManager } from './tool-calling.js';
76+import { accountStorage } from './util/AccountStorage.js';
7677
7778export {
7879 openai_messages_count,
@@ -414,7 +415,7 @@ async function validateReverseProxy() {
414415 throw err;
415416 }
416417 const rememberKey = `Proxy_SkipConfirm_${getStringHash(oai_settings.reverse_proxy)}`;
417418 const skipConfirm = localStorageaccountStorage.getItem(rememberKey) === 'true';
418419
419420 const confirmation = skipConfirm || await Popup.show.confirm(t`Connecting To Proxy`, await renderTemplateAsync('proxyConnectionWarning', { proxyURL: DOMPurify.sanitize(oai_settings.reverse_proxy) }));
420421
@@ -425,7 +426,7 @@ async function validateReverseProxy() {
425426 throw new Error('Proxy connection denied.');
426427 }
427428
428429 localStorageaccountStorage.setItem(rememberKey, String(true));
429430}
430431
431432/**
public/scripts/personas.js+6 -5
@@ -25,6 +25,7 @@ import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
2525import { t } from './i18n.js';
2626import { openWorldInfoEditor, world_names } from './world-info.js';
2727import { renderTemplateAsync } from './templates.js';
28+import { accountStorage } from './util/AccountStorage.js';
2829
2930let savePersonasPage = 0;
3031const GRID_STORAGE_KEY = 'Personas_GridView';
@@ -34,7 +35,7 @@ export let user_avatar = '';
3435export const personasFilter = new FilterHelper(debounce(getUserAvatars, debounce_timeout.quick));
3536
3637function switchPersonaGridView() {
3738 const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true';
3839 $('#user_avatar_block').toggleClass('gridView', state);
3940}
4041
@@ -182,7 +183,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
182183
183184 const storageKey = 'Personas_PerPage';
184185 const listId = '#user_avatar_block';
185186 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 5;
186187
187188 $('#persona_pagination_container').pagination({
188189 dataSource: entities,
@@ -205,7 +206,7 @@ export async function getUserAvatars(doRender = true, openPageAt = '') {
205206 highlightSelectedAvatar();
206207 },
207208 afterSizeSelectorChange: function (e) {
208209 localStorageaccountStorage.setItem(storageKey, e.target.value);
209210 },
210211 afterPaging: function (e) {
211212 savePersonasPage = e;
@@ -1132,8 +1133,8 @@ export function initPersonas() {
11321133 saveSettingsDebounced();
11331134 });
11341135 $('#persona_grid_toggle').on('click', () => {
11351136 const state = localStorageaccountStorage.getItem(GRID_STORAGE_KEY) === 'true';
11361137 localStorageaccountStorage.setItem(GRID_STORAGE_KEY, String(!state));
11371138 switchPersonaGridView();
11381139 });
11391140
public/scripts/power-user.js+3 -2
@@ -54,6 +54,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
5454import { POPUP_TYPE, callGenericPopup } from './popup.js';
5555import { loadSystemPrompts } from './sysprompt.js';
5656import { fuzzySearchCategories } from './filters.js';
57+import { accountStorage } from './util/AccountStorage.js';
5758
5859export {
5960 loadPowerUserSettings,
@@ -2019,7 +2020,7 @@ export function renderStoryString(params) {
20192020 */
20202021function validateStoryString(storyString, params) {
20212022 /** @type {{hashCache: {[hash: string]: {fieldsWarned: {[key: string]: boolean}}}}} */
20222023 const cache = JSON.parse(localStorageaccountStorage.getItem(storage_keys.storyStringValidationCache)) ?? { hashCache: {} };
20232024
20242025 const hash = getStringHash(storyString);
20252026
@@ -2056,7 +2057,7 @@ function validateStoryString(storyString, params) {
20562057 toastr.warning(`The story string does not contain the following fields, but they would contain content: ${fieldsList}`, 'Story String Validation');
20572058 }
20582059
20592060 localStorageaccountStorage.setItem(storage_keys.storyStringValidationCache, JSON.stringify(cache));
20602061}
20612062
20622063
public/scripts/slash-commands.js+3 -2
@@ -75,6 +75,7 @@ import { commonEnumProviders, enumIcons } from './slash-commands/SlashCommandCom
7575import { SlashCommandBreakController } from './slash-commands/SlashCommandBreakController.js';
7676import { SlashCommandExecutionError } from './slash-commands/SlashCommandExecutionError.js';
7777import { slashCommandReturnHelper } from './slash-commands/SlashCommandReturnHelper.js';
78+import { accountStorage } from './util/AccountStorage.js';
7879export {
7980 executeSlashCommands, executeSlashCommandsWithOptions, getSlashCommandsHelp, registerSlashCommand,
8081};
@@ -3606,9 +3607,9 @@ export async function sendMessageAs(args, text) {
36063607
36073608 if (!name) {
36083609 const namelessWarningKey = 'sendAsNamelessWarningShown';
36093610 if (localStorageaccountStorage.getItem(namelessWarningKey) !== 'true') {
36103611 toastr.warning('To avoid confusion, please use /sendas name="Character Name"', 'Name defaulted to {{char}}', { timeOut: 10000 });
36113612 localStorageaccountStorage.setItem(namelessWarningKey, 'true');
36123613 }
36133614 name = name2;
36143615 }
public/scripts/st-context.js+2 -0
@@ -68,12 +68,14 @@ import { tag_map, tags } from './tags.js';
6868import { textgenerationwebui_settings } from './textgen-settings.js';
6969import { tokenizers, getTextTokens, getTokenCount, getTokenCountAsync, getTokenizerModel } from './tokenizers.js';
7070import { ToolManager } from './tool-calling.js';
71+import { accountStorage } from './util/AccountStorage.js';
7172import { timestampToMoment, uuidv4 } from './utils.js';
7273import { getGlobalVariable, getLocalVariable, setGlobalVariable, setLocalVariable } from './variables.js';
7374import { convertCharacterBook, loadWorldInfo, saveWorldInfo, updateWorldInfoList } from './world-info.js';
7475
7576export function getContext() {
7677 return {
78+ accountStorage,
7779 chat,
7880 characters,
7981 groups,
public/scripts/textgen-models.js+4 -3
@@ -6,6 +6,7 @@ import { tokenizers } from './tokenizers.js';
66import { renderTemplateAsync } from './templates.js';
77import { POPUP_TYPE, callGenericPopup } from './popup.js';
88import { t } from './i18n.js';
9+import { accountStorage } from './util/AccountStorage.js';
910
1011let mancerModels = [];
1112let togetherModels = [];
@@ -336,7 +337,7 @@ export async function loadFeatherlessModels(data) {
336337 populateClassSelection(data);
337338
338339 // Retrieve the stored number of items per page or default to 10
339340 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || 10;
340341
341342 // Initialize pagination
342343 applyFiltersAndSort();
@@ -412,7 +413,7 @@ export async function loadFeatherlessModels(data) {
412413 },
413414 afterSizeSelectorChange: function (e) {
414415 const newPerPage = e.target.value;
415416 localStorageaccountStorage.setItem('Models_PerPage'storageKey, newPerPage);
416417 setupPagination(models, Number(newPerPage), featherlessCurrentPage); // Use the stored current page number
417418 },
418419 });
@@ -513,7 +514,7 @@ export async function loadFeatherlessModels(data) {
513514 const currentModelIndex = filteredModels.findIndex(x => x.id === textgen_settings.featherless_model);
514515 featherlessCurrentPage = currentModelIndex >= 0 ? (currentModelIndex / perPage) + 1 : 1;
515516
516517 setupPagination(filteredModels, Number(localStorageaccountStorage.getItem(storageKey)) || perPage, featherlessCurrentPage);
517518 }
518519
519520 // Required to keep the /model command function
public/scripts/user.js+8 -0
@@ -44,6 +44,14 @@ export function isAdmin() {
4444}
4545
4646/**
47+ * Gets the handle string of the current user.
48+ * @returns {string} User handle
49+ */
50+export function getCurrentUserHandle() {
51+ return currentUser?.handle || 'default-user';
52+}
53+
54+/**
4755 * Get the current user.
4856 * @returns {Promise<void>}
4957 */
public/scripts/util/AccountStorage.js+137 -0
@@ -0,0 +1,137 @@
1+import { saveSettingsDebounced } from '../../script.js';
2+
3+const MIGRATED_MARKER = '__migrated';
4+const MIGRATABLE_KEYS = [
5+ /^AlertRegex_/,
6+ /^AlertWI_/,
7+ /^Assets_SkipConfirm_/,
8+ /^Characters_PerPage$/,
9+ /^DataBank_sortField$/,
10+ /^DataBank_sortOrder$/,
11+ /^extension_update_nag$/,
12+ /^extensions_sortByName$/,
13+ /^FeatherlessModels_PerPage$/,
14+ /^GroupMembers_PerPage$/,
15+ /^GroupCandidates_PerPage$/,
16+ /^LNavLockOn$/,
17+ /^LNavOpened$/,
18+ /^mediaWarningShown:/,
19+ /^NavLockOn$/,
20+ /^NavOpened$/,
21+ /^Personas_PerPage$/,
22+ /^Personas_GridView$/,
23+ /^Proxy_SkipConfirm_/,
24+ /^qr--executeShortcut$/,
25+ /^qr--syntax$/,
26+ /^qr--tabSize$/,
27+ /^qr--wrap$/,
28+ /^RegenerateWithCtrlEnter$/,
29+ /^SelectedNavTab$/,
30+ /^sendAsNamelessWarningShown$/,
31+ /^StoryStringValidationCache$/,
32+ /^WINavOpened$/,
33+ /^WI_PerPage$/,
34+ /^world_info_sort_order$/,
35+];
36+
37+/**
38+ * Provides access to account storage of arbitrary key-value pairs.
39+ */
40+class AccountStorage {
41+ /**
42+ * @type {Record<string, string>} Storage state
43+ */
44+ #state = {};
45+
46+ /**
47+ * @type {boolean} If the storage was initialized
48+ */
49+ #ready = false;
50+
51+ #migrateLocalStorage() {
52+ for (let i = 0; i < globalThis.localStorage.length; i++) {
53+ const key = globalThis.localStorage.key(i);
54+ const value = globalThis.localStorage.getItem(key);
55+
56+ if (MIGRATABLE_KEYS.some(k => k.test(key))) {
57+ this.#state[key] = value;
58+ globalThis.localStorage.removeItem(key);
59+ }
60+ }
61+ }
62+
63+ /**
64+ * Initialize the account storage.
65+ * @param {Object} state Initial state
66+ */
67+ init(state) {
68+ if (state && typeof state === 'object') {
69+ this.#state = Object.assign(this.#state, state);
70+ }
71+
72+ if (!Object.hasOwn(this.#state, MIGRATED_MARKER)) {
73+ this.#migrateLocalStorage();
74+ this.#state[MIGRATED_MARKER] = '1';
75+ saveSettingsDebounced();
76+ }
77+
78+ this.#ready = true;
79+ }
80+
81+ /**
82+ * Get the value of a key in account storage.
83+ * @param {string} key Key to get
84+ * @returns {string|null} Value of the key
85+ */
86+ getItem(key) {
87+ if (!this.#ready) {
88+ console.warn(`AccountStorage not ready (trying to read from ${key})`);
89+ }
90+
91+ return Object.hasOwn(this.#state, key) ? String(this.#state[key]) : null;
92+ }
93+
94+ /**
95+ * Set a key in account storage.
96+ * @param {string} key Key to set
97+ * @param {string} value Value to set
98+ */
99+ setItem(key, value) {
100+ if (!this.#ready) {
101+ console.warn(`AccountStorage not ready (trying to write to ${key})`);
102+ }
103+
104+ this.#state[key] = String(value);
105+ saveSettingsDebounced();
106+ }
107+
108+ /**
109+ * Remove a key from account storage.
110+ * @param {string} key Key to remove
111+ */
112+ removeItem(key) {
113+ if (!this.#ready) {
114+ console.warn(`AccountStorage not ready (trying to remove ${key})`);
115+ }
116+
117+ if (!Object.hasOwn(this.#state, key)) {
118+ return;
119+ }
120+
121+ delete this.#state[key];
122+ saveSettingsDebounced();
123+ }
124+
125+ /**
126+ * Gets a snapshot of the storage state.
127+ * @returns {Record<string, string>} A deep clone of the storage state
128+ */
129+ getState() {
130+ return structuredClone(this.#state);
131+ }
132+}
133+
134+/**
135+ * Account storage instance.
136+ */
137+export const accountStorage = new AccountStorage();
public/scripts/world-info.js+9 -8
@@ -21,6 +21,7 @@ import { callGenericPopup, Popup, POPUP_TYPE } from './popup.js';
2121import { StructuredCloneMap } from './util/StructuredCloneMap.js';
2222import { renderTemplateAsync } from './templates.js';
2323import { t } from './i18n.js';
24+import { accountStorage } from './util/AccountStorage.js';
2425
2526export const world_info_insertion_strategy = {
2627 evenly: 0,
@@ -872,7 +873,7 @@ export function setWorldInfoSettings(settings, data) {
872873 $('#world_editor_select').append(`<option value='${i}'>${item}</option>`);
873874 });
874875
875876 $('#world_info_sort_order').val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0');
876877 $('#world_info').trigger('change');
877878 $('#world_editor_select').trigger('change');
878879
@@ -1947,13 +1948,13 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19471948 if (typeof navigation === 'number' && Number(navigation) >= 0) {
19481949 const data = getDataArray();
19491950 const uidIndex = data.findIndex(x => x.uid === navigation);
19501951 const perPage = Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault;
19511952 startPage = Math.floor(uidIndex / perPage) + 1;
19521953 }
19531954
19541955 $('#world_info_pagination').pagination({
19551956 dataSource: getDataArray,
19561957 pageSize: Number(localStorageaccountStorage.getItem(storageKey)) || perPageDefault,
19571958 sizeChangerOptions: [10, 25, 50, 100, 500, 1000],
19581959 showSizeChanger: true,
19591960 pageRange: 1,
@@ -1983,7 +1984,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none, fl
19831984 worldEntriesList.append(blocks);
19841985 },
19851986 afterSizeSelectorChange: function (e) {
19861987 localStorageaccountStorage.setItem(storageKey, e.target.value);
19871988 },
19881989 afterPaging: function () {
19891990 $('#world_popup_entries_list textarea[name="comment"]').each(function () {
@@ -2188,7 +2189,7 @@ function verifyWorldInfoSearchSortRule() {
21882189 // If search got cleared, we make sure to hide the option and go back to the one before
21892190 if (!searchTerm && !isHidden) {
21902191 searchOption.attr('hidden', '');
21912192 selector.val(localStorageaccountStorage.getItem(SORT_ORDER_KEY) || '0');
21922193 }
21932194}
21942195
@@ -4753,8 +4754,8 @@ export function checkEmbeddedWorld(chid) {
47534754 // Only show the alert once per character
47544755 const checkKey = `AlertWI_${characters[chid].avatar}`;
47554756 const worldName = characters[chid]?.data?.extensions?.world;
47564757 if (!localStorageaccountStorage.getItem(checkKey) && (!worldName || !world_names.includes(worldName))) {
47574758 localStorageaccountStorage.setItem(checkKey, 'true');
47584759
47594760 if (power_user.world_import_dialog) {
47604761 const html = `<h3>This character has an embedded World/Lorebook.</h3>
@@ -5198,7 +5199,7 @@ jQuery(() => {
51985199 $('#world_info_sort_order').on('change', function () {
51995200 const value = String($(this).find(':selected').val());
52005201 // Save sort order, but do not save search sorting, as this is a temporary sorting option
52015202 if (value !== 'search') localStorageaccountStorage.setItem(SORT_ORDER_KEY, value);
52025203 updateEditor(navigation_option.none);
52035204 });
52045205