Add settings.json-backed KV string storage Fixes #3461, #3443

d5bdf1cb90583a475670e9ce03dd9605dd392204

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

16 files changed, +205 -99Ignore whitespace
public/script.js+12 -9
@@ -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, 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;
@@ -6902,10 +6903,11 @@ export async function getSettings() {
69026903 $('#your_name').val(name1);
69036904 }
69046905
6906+ accountStorage.init(settings?.accountStorage);
69056907 await setUserControls(data.enable_accounts);
69066908
69076909 // Allow subscribers to mutate settings
69086910 await eventSource.emit(event_types.SETTINGS_LOADED_BEFORE, settings);
69096911
69106912 //Load KoboldAI settings
69116913 koboldai_setting_names = data.koboldai_setting_names;
@@ -7094,6 +7096,7 @@ export async function saveSettings(loopCounter = 0) {
70947096 url: '/api/settings/save',
70957097 data: JSON.stringify({
70967098 firstRun: firstRun,
7099+ accountStorage: accountStorage.state,
70977100 currentVersion: currentVersion,
70987101 username: name1,
70997102 active_character: active_character,
@@ -7549,7 +7552,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
75497552 }
75507553
75517554 try {
75527555 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
75537556 const page = Math.floor(charIndex / perPage) + 1;
75547557 const selector = `#rm_print_characters_block [title*="${avatarFileName}"]`;
75557558 $('#rm_print_characters_pagination').pagination('go', page);
@@ -7581,7 +7584,7 @@ export function select_rm_info(type, charId, previousCharId = null) {
75817584 return;
75827585 }
75837586
75847587 const perPage = Number(localStorageaccountStorage.getItem('Characters_PerPage')) || per_page_default;
75857588 const page = Math.floor(charIndex / perPage) + 1;
75867589 $('#rm_print_characters_pagination').pagination('go', page);
75877590 const selector = `#rm_print_characters_block [grid="${charId}"]`;
@@ -9650,8 +9653,8 @@ function addDebugFunctions() {
96509653 });
96519654
96529655 registerDebugFunction('toggleRegenerateWarning', 'Toggle Ctrl+Enter regeneration confirmation', 'Toggle the warning when regenerating a message with a Ctrl+Enter hotkey.', () => {
96539656 localStorageaccountStorage.setItem('RegenerateWithCtrlEnter', localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'false' : 'true');
96549657 toastr.info('Regenerate warning is now ' + (localStorageaccountStorage.getItem('RegenerateWithCtrlEnter') === 'true' ? 'disabled' : 'enabled'));
96559658 });
96569659
96579660 registerDebugFunction('copySetup', 'Copy ST setup to clipboard [WIP]', 'Useful data when reporting bugs', async () => {
public/scripts/RossAscends-mods.js+26 -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,7 @@ 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';
4444
4545var RPanelPin = document.getElementById('rm_button_panel_pin');
4646var LPanelPin = document.getElementById('lm_button_panel_pin');
@@ -409,19 +409,19 @@ function RA_autoconnect(PrevApi) {
409409function OpenNavPanels() {
410410 if (!isMobile()) {
411411 //auto-open R nav if locked and previously open
412412 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('NavOpened') == 'true') {
413413 //console.log("RA -- clicking right nav to open");
414414 $('#rightNavDrawerIcon').click();
415415 }
416416
417417 //auto-open L nav if locked and previously open
418418 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('LNavOpened') == 'true') {
419419 console.debug('RA -- clicking left nav to open');
420420 $('#leftNavDrawerIcon').click();
421421 }
422422
423423 //auto-open WI if locked and previously open
424424 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true' && LoadLocalBoolaccountStorage.getItem('WINavOpened') == 'true') {
425425 console.debug('RA -- clicking WI to open');
426426 $('#WIDrawerIcon').click();
427427 }
@@ -434,7 +434,7 @@ function restoreUserInput() {
434434 return;
435435 }
436436
437437 const userInput = LoadLocallocalStorage.getItem('userInput');
438438 if (userInput) {
439439 $('#send_textarea').val(userInput)[0].dispatchEvent(new Event('input', { bubbles: true }));
440440 }
@@ -442,7 +442,8 @@ function restoreUserInput() {
442442
443443function saveUserInput() {
444444 const userInput = String($('#send_textarea').val());
445445 SaveLocallocalStorage.setItem('userInput', userInput);
446+ console.debug('User Input -- ', userInput);
446447}
447448const saveUserInputDebounced = debounce(saveUserInput);
448449
@@ -739,7 +740,7 @@ export function initRossMods() {
739740
740741 //toggle pin class when lock toggle clicked
741742 $(RPanelPin).on('click', function () {
742743 SaveLocalaccountStorage.setItem('NavLockOn', $(RPanelPin).prop('checked'));
743744 if ($(RPanelPin).prop('checked') == true) {
744745 //console.log('adding pin class to right nav');
745746 $(RightNavPanel).addClass('pinnedOpen');
@@ -757,7 +758,7 @@ export function initRossMods() {
757758 }
758759 });
759760 $(LPanelPin).on('click', function () {
760761 SaveLocalaccountStorage.setItem('LNavLockOn', $(LPanelPin).prop('checked'));
761762 if ($(LPanelPin).prop('checked') == true) {
762763 //console.log('adding pin class to Left nav');
763764 $(LeftNavPanel).addClass('pinnedOpen');
@@ -776,7 +777,7 @@ export function initRossMods() {
776777 });
777778
778779 $(WIPanelPin).on('click', function () {
779780 SaveLocalaccountStorage.setItem('WINavLockOn', $(WIPanelPin).prop('checked'));
780781 if ($(WIPanelPin).prop('checked') == true) {
781782 console.debug('adding pin class to WI');
782783 $(WorldInfo).addClass('pinnedOpen');
@@ -796,8 +797,8 @@ export function initRossMods() {
796797 });
797798
798799 // read the state of right Nav Lock and apply to rightnav classlist
799800 $(RPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true');
800801 if (LoadLocalBoolaccountStorage.getItem('NavLockOn') == 'true') {
801802 //console.log('setting pin class via local var');
802803 $(RightNavPanel).addClass('pinnedOpen');
803804 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -808,8 +809,8 @@ export function initRossMods() {
808809 $(RightNavDrawerIcon).addClass('drawerPinnedOpen');
809810 }
810811 // read the state of left Nav Lock and apply to leftnav classlist
811812 $(LPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('LNavLockOn') === 'true');
812813 if (LoadLocalBoolaccountStorage.getItem('LNavLockOn') == 'true') {
813814 //console.log('setting pin class via local var');
814815 $(LeftNavPanel).addClass('pinnedOpen');
815816 $(LeftNavDrawerIcon).addClass('drawerPinnedOpen');
@@ -821,8 +822,8 @@ export function initRossMods() {
821822 }
822823
823824 // read the state of left Nav Lock and apply to leftnav classlist
824825 $(WIPanelPin).prop('checked', LoadLocalBoolaccountStorage.getItem('WINavLockOn') === 'true');
825826 if (LoadLocalBoolaccountStorage.getItem('WINavLockOn') == 'true') {
826827 //console.log('setting pin class via local var');
827828 $(WorldInfo).addClass('pinnedOpen');
828829 $(WIDrawerIcon).addClass('drawerPinnedOpen');
@@ -837,22 +838,22 @@ export function initRossMods() {
837838 //save state of Right nav being open or closed
838839 $('#rightNavDrawerIcon').on('click', function () {
839840 if (!$('#rightNavDrawerIcon').hasClass('openIcon')) {
840841 SaveLocalaccountStorage.setItem('NavOpened', 'true');
841842 } else { SaveLocalaccountStorage.setItem('NavOpened', 'false'); }
842843 });
843844
844845 //save state of Left nav being open or closed
845846 $('#leftNavDrawerIcon').on('click', function () {
846847 if (!$('#leftNavDrawerIcon').hasClass('openIcon')) {
847848 SaveLocalaccountStorage.setItem('LNavOpened', 'true');
848849 } else { SaveLocalaccountStorage.setItem('LNavOpened', 'false'); }
849850 });
850851
851852 //save state of Left nav being open or closed
852853 $('#WorldInfo').on('click', function () {
853854 if (!$('#WorldInfo').hasClass('openIcon')) {
854855 SaveLocalaccountStorage.setItem('WINavOpened', 'true');
855856 } else { SaveLocalaccountStorage.setItem('WINavOpened', 'false'); }
856857 });
857858
858859 var chatbarInFocus = false;
@@ -868,8 +869,8 @@ export function initRossMods() {
868869 OpenNavPanels();
869870 }, 300);
870871
871872 $(SelectedCharacterTab).click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_selected_ch'); });
872873 $('#rm_button_characters').click(function () { SaveLocalaccountStorage.setItem('SelectedNavTab', 'rm_button_characters'); });
873874
874875 // when a char is selected from the list, save them as the auto-load character for next page load
875876
@@ -1077,7 +1078,7 @@ export function initRossMods() {
10771078 }
10781079 else if (is_send_press == false) {
10791080 const skipConfirmKey = 'RegenerateWithCtrlEnter';
10801081 const skipConfirm = LoadLocalBoolaccountStorage.getItem(skipConfirmKey) === 'true';
10811082 function doRegenerate() {
10821083 console.debug('Regenerating with Ctrl+Enter');
10831084 $('#option_regenerate').trigger('click');
@@ -1097,7 +1098,7 @@ export function initRossMods() {
10971098 return;
10981099 }
10991100
11001101 SaveLocalaccountStorage.setItem(skipConfirmKey, String(regenerateWithCtrlEnter));
11011102 doRegenerate();
11021103 }
11031104 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/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/util/AccountStorage.js+116 -0
@@ -0,0 +1,116 @@
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+ /^RegenerateWithCtrlEnter$/,
25+ /^SelectedNavTab$/,
26+ /^sendAsNamelessWarningShown$/,
27+ /^StoryStringValidationCache$/,
28+ /^WINavOpened$/,
29+ /^WI_PerPage$/,
30+ /^world_info_sort_order$/,
31+];
32+
33+/**
34+ * Provides access to account storage of arbitrary key-value pairs.
35+ */
36+class AccountStorage {
37+ constructor() {
38+ this.state = {};
39+ this.ready = false;
40+ }
41+
42+ #migrateLocalStorage() {
43+ for (let i = 0; i < globalThis.localStorage.length; i++) {
44+ const key = globalThis.localStorage.key(i);
45+ const value = globalThis.localStorage.getItem(key);
46+
47+ if (MIGRATABLE_KEYS.some(k => k.test(key))) {
48+ this.state[key] = value;
49+ globalThis.localStorage.removeItem(key);
50+ }
51+ }
52+ }
53+
54+ /**
55+ * Initialize the account storage.
56+ * @param {Object} state Initial state
57+ */
58+ init(state) {
59+ if (state && typeof state === 'object') {
60+ this.state = Object.assign(this.state, state);
61+ }
62+
63+ if (!Object.hasOwn(this.state, MIGRATED_MARKER)) {
64+ this.#migrateLocalStorage();
65+ this.state[MIGRATED_MARKER] = 1;
66+ saveSettingsDebounced();
67+ }
68+
69+ this.ready = true;
70+ }
71+
72+ /**
73+ * Get the value of a key in account storage.
74+ * @param {string} key Key to get
75+ * @returns {string|null} Value of the key
76+ */
77+ getItem(key) {
78+ if (!this.ready) {
79+ console.warn(`AccountStorage not ready (trying to read from ${key})`);
80+ }
81+
82+ return Object.hasOwn(this.state, key) ? String(this.state[key]) : null;
83+ }
84+
85+ /**
86+ * Set a key in account storage.
87+ * @param {string} key Key to set
88+ * @param {string} value Value to set
89+ */
90+ setItem(key, value) {
91+ if (!this.ready) {
92+ console.warn(`AccountStorage not ready (trying to write to ${key})`);
93+ }
94+
95+ this.state[key] = String(value);
96+ saveSettingsDebounced();
97+ }
98+
99+ /**
100+ * Remove a key from account storage.
101+ * @param {string} key Key to remove
102+ */
103+ removeItem(key) {
104+ if (!this.ready) {
105+ console.warn(`AccountStorage not ready (trying to remove ${key})`);
106+ }
107+
108+ delete this.state[key];
109+ saveSettingsDebounced();
110+ }
111+}
112+
113+/**
114+ * Account storage instance.
115+ */
116+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