mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
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:
@ -198,7 +198,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
span.select2-container .select2-selection__choice__display:has(> .regex_item),
|
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;
|
background-color: #D27D2D30;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,3 +214,12 @@ span.select2-container .select2-results__option:has(> .regex_item) {
|
|||||||
top: -1px;
|
top: -1px;
|
||||||
margin-right: 3px;
|
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>
|
</span>
|
||||||
</small>
|
</small>
|
||||||
<small class="textAlignCenter" data-i18n="Primary Keywords">Primary Keywords</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>
|
<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>
|
||||||
<div class="world_entry_form_control">
|
<div class="world_entry_form_control">
|
||||||
@ -5128,7 +5128,7 @@
|
|||||||
</span>
|
</span>
|
||||||
</small>
|
</small>
|
||||||
<small class="textAlignCenter" data-i18n="Optional Filter">Optional Filter</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>
|
<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>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1450,7 +1450,12 @@ export function includesIgnoreCaseAndAccents(text, searchTerm) {
|
|||||||
return normalizedText.includes(normalizedSearchTerm);
|
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
|
* 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
|
* 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 {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 {object} [options] - Optional arguments
|
||||||
* @param {boolean} [options.select=false] - Whether the options should be selected right away
|
* @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
|
* @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 } = {}) {
|
export function select2ModifyOptions(element, items, { select = false, changeEventArgs = null } = {}) {
|
||||||
if (!items.length) return;
|
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 dataItems = items.map(x => typeof x === 'string' ? { id: getSelect2OptionId(x), text: x } : x);
|
||||||
|
|
||||||
|
const existingValues = [];
|
||||||
dataItems.forEach(item => {
|
dataItems.forEach(item => {
|
||||||
// Set the value, creating a new option if necessary
|
// Set the value, creating a new option if necessary
|
||||||
if (element.find("option[value='" + item.id + "']").length) {
|
if (element.find("option[value='" + item.id + "']").length) {
|
||||||
if (select) element.val(item.id).trigger('change', changeEventArgs);
|
if (select) existingValues.push(item.id);
|
||||||
} else {
|
} else {
|
||||||
// Create a DOM Option and optionally pre-select by default
|
// Create a DOM Option and optionally pre-select by default
|
||||||
var newOption = new Option(item.text, item.id, select, select);
|
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);
|
element.append(newOption);
|
||||||
if (select) element.trigger('change', changeEventArgs);
|
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 { 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 { extension_settings, getContext } from './extensions.js';
|
||||||
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
import { NOTE_MODULE_NAME, metadata_keys, shouldWIAddPrompt } from './authors-note.js';
|
||||||
import { registerSlashCommand } from './slash-commands.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 });
|
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) {
|
function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
||||||
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
updateEditor = (navigation) => displayWorldEntries(name, data, navigation);
|
||||||
|
|
||||||
@ -867,6 +898,10 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||||
entriesArray = sortEntries(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
|
// Run the callback for printing this
|
||||||
typeof callback === 'function' && callback(entriesArray);
|
typeof callback === 'function' && callback(entriesArray);
|
||||||
return entriesArray;
|
return entriesArray;
|
||||||
@ -1146,11 +1181,7 @@ function deleteOriginalDataValue(data, uid) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/** @typedef {import('./utils.js').Select2Option} Select2Option */
|
||||||
* @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
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Splits a given input string that contains one or more keywords or regexes, separated by commas.
|
* 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();
|
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);
|
const isRegex = isValidRegex(item.text);
|
||||||
if (!isRegex) return item.text;
|
if (isRegex) {
|
||||||
return $('<span>').addClass('regex_item').text(item.text)
|
content.addClass('regex_item').prepend($('<span>').addClass('regex_icon').text("•*").attr('title', 'Regex'));
|
||||||
.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()) {
|
if (!isMobile()) {
|
||||||
input.select2({
|
input.select2({
|
||||||
|
ajax: getDynamicSelect2DataViaAjax(() => worldEntryKeyOptionsCache),
|
||||||
tags: true,
|
tags: true,
|
||||||
tokenSeparators: [','],
|
tokenSeparators: [','],
|
||||||
tokenizer: customTokenizer,
|
tokenizer: customTokenizer,
|
||||||
placeholder: input.attr('placeholder'),
|
placeholder: input.attr('placeholder'),
|
||||||
templateResult: templateStyling,
|
templateResult: item => templateStyling(item, { searchStyle: true }),
|
||||||
templateSelection: templateStyling,
|
templateSelection: item => templateStyling(item),
|
||||||
});
|
});
|
||||||
input.on('change', function (_, { skipReset, noSave } = {}) {
|
input.on('change', function (_, { skipReset, noSave } = {}) {
|
||||||
const uid = $(this).data('uid');
|
const uid = $(this).data('uid');
|
||||||
@ -1325,6 +1368,8 @@ function getWorldEntry(name, data, entry) {
|
|||||||
saveWorldInfo(name, data);
|
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 } });
|
select2ModifyOptions(input, entry[entryPropName], { select: true, changeEventArgs: { skipReset: true, noSave: true } });
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user