WI key dropdown templating shows all keys
- Cache all keys for the loaded lorebook - Key selection dropdown shows all keys and how often they are used already - More templating changes
This commit is contained in:
parent
fda0e886e4
commit
eb273a1873
|
@ -198,7 +198,7 @@
|
|||
}
|
||||
|
||||
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
||||
span.select2-container .select2-results__option:has(> .regex_item) {
|
||||
span.select2-container .select2-results__option:has(> .result_block .regex_item) {
|
||||
background-color: #D27D2D30;
|
||||
}
|
||||
|
||||
|
@ -213,4 +213,13 @@ span.select2-container .select2-results__option:has(> .regex_item) {
|
|||
position: relative;
|
||||
top: -1px;
|
||||
margin-right: 3px;
|
||||
}
|
||||
|
||||
.select2-results__option .regex_item .regex_icon {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.select2-results__option .item_count {
|
||||
margin-left: 10px;
|
||||
float: right;
|
||||
}
|
|
@ -5109,7 +5109,7 @@
|
|||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</small>
|
||||
<select class="keyprimaryselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
|
||||
<select class="keyprimaryselect keyselect select2_multi_sameline" name="key" data-i18n="[placeholder]Keywords or Regexes" placeholder="Keywords or Regexes" multiple="multiple"></select>
|
||||
<textarea class="text_pole keyprimarytextpole mobile" name="key" rows="1" data-i18n="[placeholder]Comma separated list" placeholder="Comma separated list" maxlength="2000" style="display: none;"></textarea>
|
||||
</div>
|
||||
<div class="world_entry_form_control">
|
||||
|
@ -5128,7 +5128,7 @@
|
|||
</span>
|
||||
</small>
|
||||
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</small>
|
||||
<select class="keypsecondaryselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
|
||||
<select class="keysecondaryselect keyselect select2_multi_sameline" name="keysecondary" data-i18n="[placeholder]Keywords or Regexes (ignored if empty)" placeholder="Keywords or Regexes (ignored if empty)" multiple="multiple"></select>
|
||||
<textarea class="text_pole keysecondarytextpole mobile" name="keysecondary" rows="1" data-i18n="[placeholder]Comma separated list (ignored if empty)" placeholder="Comma separated list (ignored if empty)" maxlength="2000" style="display: none;"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1450,7 +1450,12 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
|||
return normalizedText.includes(normalizedSearchTerm);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {object} Select2Option The option object for select2 controls
|
||||
* @property {string} id - The unique ID inside this select
|
||||
* @property {string} text - The text for this option
|
||||
* @property {number?} [count] - Optionally show the count how often that option was chosen already
|
||||
*/
|
||||
|
||||
/**
|
||||
* Returns a unique hash as ID for a select2 option text
|
||||
|
@ -1466,20 +1471,21 @@ export function getSelect2OptionId(option) {
|
|||
* Modifies the select2 options by adding not existing one and optionally selecting them
|
||||
*
|
||||
* @param {JQuery<HTMLElement>} element - The "select" element to add the options to
|
||||
* @param {string[]|{id: string, text: string}[]} items - The option items to build, add or select
|
||||
* @param {string[]|Select2Option[]} items - The option items to build, add or select
|
||||
* @param {object} [options] - Optional arguments
|
||||
* @param {boolean} [options.select=false] - Whether the options should be selected right away
|
||||
* @param {object} [options.changeEventArgs=null] - Optional event args being passed into the "change" event when its triggered because a new options is selected
|
||||
*/
|
||||
export function select2ModifyOptions(element, items, { select = false, changeEventArgs = null } = {}) {
|
||||
if (!items.length) return;
|
||||
/** @type {{id: string, text: string}[]} */
|
||||
/** @type {Select2Option[]} */
|
||||
const dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
|
||||
const existingValues = [];
|
||||
dataItems.forEach(item => {
|
||||
// Set the value, creating a new option if necessary
|
||||
if (element.find("option[value='" + item.id + "']").length) {
|
||||
if (select) element.val(item.id).trigger('change', changeEventArgs);
|
||||
if (select) existingValues.push(item.id);
|
||||
} else {
|
||||
// Create a DOM Option and optionally pre-select by default
|
||||
var newOption = new Option(item.text, item.id, select, select);
|
||||
|
@ -1487,5 +1493,34 @@ export function select2ModifyOptions(element, items, { select = false, changeEve
|
|||
element.append(newOption);
|
||||
if (select) element.trigger('change', changeEventArgs);
|
||||
}
|
||||
if (existingValues.length) element.val(existingValues).trigger('change', changeEventArgs);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the ajax settings that can be used on the select2 ajax property to dynamically get the data.
|
||||
* Can be used on a single global array, querying data from the server or anything similar.
|
||||
*
|
||||
* @param {function():Select2Option[]} dataProvider - The provider/function to retrieve the data - can be as simple as "() => myData" for arrays
|
||||
* @return {object} The ajax object with the transport function to use on the select2 ajax property
|
||||
*/
|
||||
export function getDynamicSelect2DataViaAjax(dataProvider) {
|
||||
function dynamicSelect2DataTransport(params, success, failure) {
|
||||
var items = dataProvider();
|
||||
// fitering if params.data.q available
|
||||
if (params.data && params.data.q) {
|
||||
items = items.filter(function (item) {
|
||||
return new RegExp(params.data.q).test(item.text);
|
||||
});
|
||||
}
|
||||
var promise = new Promise(function (resolve, reject) {
|
||||
resolve({ results: items });
|
||||
});
|
||||
promise.then(success);
|
||||
promise.catch(failure);
|
||||
};
|
||||
const ajax = {
|
||||
transport: dynamicSelect2DataTransport
|
||||
};
|
||||
return ajax;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { saveSettings, callPopup, substituteParams, getRequestHeaders, chat_metadata, this_chid, characters, saveCharacterDebounced, menu_type, eventSource, event_types, getExtensionPromptByName, saveMetadata, getCurrentChatId, extension_prompt_roles } from '../script.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getStringHash, getSelect2OptionId } from './utils.js';
|
||||
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight, select2ModifyOptions, getStringHash, getSelect2OptionId, getDynamicSelect2DataViaAjax } from './utils.js';
|
||||
import { extension_settings, getContext } from './extensions.js';
|
||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||
import { registerSlashCommand } from './slash-commands.js';
|
||||
|
@ -831,6 +831,37 @@ function nullWorldInfo() {
|
|||
toastr.info('Create or import a new World Info file first.', 'World Info is not set', { timeOut: 10000, preventDuplicates: true });
|
||||
}
|
||||
|
||||
/** @type {Select2Option[]} Cache all keys as selectable dropdown option */
|
||||
const worldEntryKeyOptionsCache = [];
|
||||
|
||||
/**
|
||||
* Update the cache and all select options for the keys with new values to display
|
||||
* @param {string[]|Select2Option[]} keyOptions - An array of options to update
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean?} [options.remove=false] - Whether the option was removed, so the count should be reduced - otherwise it'll be increased
|
||||
* @param {boolean?} [options.reset=false] - Whether the cache should be reset. Reset will also not trigger update of the controls, as we expect them to be redrawn anyway
|
||||
*/
|
||||
function updateWorldEntryKeyOptionsCache(keyOptions, { remove = false, reset = false } = {}) {
|
||||
if (!keyOptions.length) return;
|
||||
/** @type {Select2Option[]} */
|
||||
const options = keyOptions.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||
if (reset) worldEntryKeyOptionsCache.length = 0;
|
||||
options.forEach(option => {
|
||||
// Update the cache list
|
||||
let cachedEntry = worldEntryKeyOptionsCache.find(x => x.id == option.id);
|
||||
if (cachedEntry) {
|
||||
cachedEntry.count += !remove ? 1 : -1;
|
||||
} else if (!remove) {
|
||||
worldEntryKeyOptionsCache.push(option);
|
||||
cachedEntry = option;
|
||||
cachedEntry.count = 1;
|
||||
}
|
||||
});
|
||||
|
||||
// Sort by count DESC and then alphabetically
|
||||
worldEntryKeyOptionsCache.sort((a, b) => b.count - a.count || a.text.localeCompare(b.text));
|
||||
}
|
||||
|
||||
function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
||||
|
||||
|
@ -867,6 +898,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||
entriesArray = sortEntries(entriesArray);
|
||||
|
||||
// Cache keys
|
||||
const keys = entriesArray.flatMap(entry => [...entry.key, ...entry.keysecondary]);
|
||||
updateWorldEntryKeyOptionsCache(keys, { reset: true });
|
||||
|
||||
// Run the callback for printing this
|
||||
typeof callback === 'function' && callback(entriesArray);
|
||||
return entriesArray;
|
||||
|
@ -1146,11 +1181,7 @@ function deleteOriginalDataValue(data, uid) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef {object} Select2Option The option object for select2 controls
|
||||
* @property {string} id - The unique ID inside this select
|
||||
* @property {string} text - The text for this option
|
||||
*/
|
||||
/** @typedef {import('./utils.js').Select2Option} Select2Option */
|
||||
|
||||
/**
|
||||
* Splits a given input string that contains one or more keywords or regexes, separated by commas.
|
||||
|
@ -1297,21 +1328,33 @@ function getWorldEntry(name, data, entry) {
|
|||
event.stopPropagation();
|
||||
});
|
||||
|
||||
function templateStyling(/** @type {Select2Option} */ item) {
|
||||
function templateStyling(/** @type {Select2Option} */ item, { searchStyle = false } = {}) {
|
||||
const content = $('<span>').addClass('item').text(item.text);
|
||||
const isRegex = isValidRegex(item.text);
|
||||
if (!isRegex) return item.text;
|
||||
return $('<span>').addClass('regex_item').text(item.text)
|
||||
.prepend($('<span>').addClass('regex_icon').text("•*").attr('title', 'Regex'));
|
||||
if (isRegex) {
|
||||
content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text("•*").attr('title', 'Regex'));
|
||||
}
|
||||
|
||||
if (searchStyle && item.count) {
|
||||
// Build a wrapping element
|
||||
const wrapper = $('<span>').addClass('result_block')
|
||||
.append(content);
|
||||
wrapper.append($('<span>').addClass('item_count').text(item.count).attr('title', `Used as a key ${item.count} ${item.count != 1 ? 'times' : 'time'} in this lorebook`));
|
||||
return wrapper;
|
||||
}
|
||||
|
||||
return content;
|
||||
}
|
||||
|
||||
if (!isMobile()) {
|
||||
input.select2({
|
||||
ajax: getDynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
|
||||
tags: true,
|
||||
tokenSeparators: [','],
|
||||
tokenizer: customTokenizer,
|
||||
placeholder: input.attr('placeholder'),
|
||||
templateResult: templateStyling,
|
||||
templateSelection: templateStyling,
|
||||
templateResult: item => templateStyling(item, { searchStyle: true }),
|
||||
templateSelection: item => templateStyling(item),
|
||||
});
|
||||
input.on('change', function (_, { skipReset, noSave } = {}) {
|
||||
const uid = $(this).data('uid');
|
||||
|
@ -1325,6 +1368,8 @@ function getWorldEntry(name, data, entry) {
|
|||
saveWorldInfo(name, data);
|
||||
}
|
||||
});
|
||||
input.on('select2:select', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data]));
|
||||
input.on('select2:unselect', /** @type {function(*):void} */ event => updateWorldEntryKeyOptionsCache([event.params.data], { remove: true }));
|
||||
|
||||
select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue