Scored search sorting for world info

This commit is contained in:
Wolfsblvt 2024-04-30 01:39:47 +02:00
parent e4de6da5b8
commit a850352eab
5 changed files with 124 additions and 12 deletions

View File

@ -3410,6 +3410,7 @@
<div id="world_popup_delete" class="menu_button fa-solid fa-trash-can redWarningBG" title="Delete World Info" data-i18n="[title]Delete World Info"></div>
<input type="search" class="text_pole textarea_compact" data-i18n="[placeholder]Search..." id="world_info_search" placeholder="Search...">
<select id="world_info_sort_order" class="margin0">
<option data-rule="search" value="14" data-i18n="Search" hidden>Search</option>
<option data-rule="priority" value="0" data-i18n="Priority">Priority</option>
<option data-rule="custom" value="13" data-i18n="Custom">Custom</option>
<option data-order="asc" data-field="comment" value="1" data-i18n="Title A-Z">Title A-Z</option>

View File

@ -1465,7 +1465,7 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
const subCount = subEntities.length;
subEntities = filterByTagState(entities, { subForEntity: entity });
if (doFilter) {
subEntities = entitiesFilter.applyFilters(subEntities);
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
}
entity.entities = subEntities;
entity.hidden = subCount - subEntities.length;

View File

@ -1,9 +1,15 @@
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js';
import { tag_map } from './tags.js';
/**
* @typedef FilterType The filter type possible for this filter helper
* @type {'search'|'tag'|'folder'|'fav'|'group'|'world_info_search'|'persona_search'}
*/
/**
* The filter types
* @type {{ SEARCH: string, TAG: string, FOLDER: string, FAV: string, GROUP: string, WORLD_INFO_SEARCH: string, PERSONA_SEARCH: string, [key: string]: string }}
* @type {{ SEARCH: 'search', TAG: 'tag', FOLDER: 'folder', FAV: 'fav', GROUP: 'group', WORLD_INFO_SEARCH: 'world_info_search', PERSONA_SEARCH: 'persona_search'}}
*/
export const FILTER_TYPES = {
SEARCH: 'search',
@ -56,12 +62,22 @@ export function isFilterState(a, b) {
* data = filterHelper.applyFilters(data);
*/
export class FilterHelper {
/**
* Cache fuzzy search weighting scores for re-usability, sorting and stuff
*
* Contains maps of weighting numbers assigned to their uid/id, for each of the different `FILTER_TYPES`
* @type {Map<FilterType, Map<string|number,number>>}
*/
scoreCache;
/**
* Creates a new FilterHelper
* @param {Function} onDataChanged Callback to trigger when the filter data changes
*/
constructor(onDataChanged) {
this.onDataChanged = onDataChanged;
this.scoreCache = new Map();
}
/**
@ -135,7 +151,12 @@ export class FilterHelper {
}
const fuzzySearchResults = fuzzySearchWorldInfo(data, term);
return data.filter(entity => fuzzySearchResults.includes(entity.uid));
var wiScoreMap = new Map(fuzzySearchResults.map(i => [i.item?.uid, i.score]));
this.cacheScores(FILTER_TYPES.WORLD_INFO_SEARCH, wiScoreMap);
const filteredData = data.filter(entity => fuzzySearchResults.find(x => x.item === entity));
return filteredData;
}
/**
@ -310,7 +331,7 @@ export class FilterHelper {
/**
* Gets the filter data for the given filter type.
* @param {string} filterType The filter type to get data for.
* @param {FilterType} filterType The filter type to get data for.
*/
getFilterData(filterType) {
return this.filterData[filterType];
@ -318,11 +339,51 @@ export class FilterHelper {
/**
* Applies all filters to the given data.
* @param {any[]} data The data to filter.
* @param {any[]} data - The data to filter.
* @param {object} options - Optional call parameters
* @param {boolean|FilterType} [options.clearScoreCache=true] - Whether the score
* @returns {any[]} The filtered data.
*/
applyFilters(data) {
applyFilters(data, { clearScoreCache = true } = {}) {
if (clearScoreCache) this.clearScoreCache();
return Object.values(this.filterFunctions)
.reduce((data, fn) => fn(data), data);
}
/**
* Cache scores for a specific filter type
* @param {FilterType} type - The type of data being cached
* @param {Map<string|number, number>} results - The search results containing mapped item identifiers and their scores
*/
cacheScores(type, results) {
/** @type {Map<string|number, number>} */
const typeScores = this.scoreCache.get(type) || new Map();
for (const [uid, score] of results) {
typeScores.set(uid, score);
}
this.scoreCache.set(type, typeScores);
console.debug('scores chached', type, typeScores);
}
/**
* Get the cached score for an item by type and its identifier
* @param {FilterType} type The type of data
* @param {string|number} uid The unique identifier for an item
* @returns {number|undefined} The cached score, or undefined if no score is present
*/
getScore(type, uid) {
return this.scoreCache.get(type)?.get(uid) ?? undefined;
}
/**
* Clear the score cache for a specific type, or completely if no type is specified
* @param {FilterType} [type] The type of data to clear scores for. Clears all if unspecified.
*/
clearScoreCache(type) {
if (type) {
this.scoreCache.set(type, new Map());
} else {
this.scoreCache = new Map();
}
}
}

View File

@ -1861,6 +1861,7 @@ function highlightDefaultContext() {
}
export function fuzzySearchCharacters(searchValue) {
// @ts-ignore
const fuse = new Fuse(characters, {
keys: [
{ name: 'data.name', weight: 8 },
@ -1885,7 +1886,14 @@ export function fuzzySearchCharacters(searchValue) {
return indices;
}
/**
* Fuzzy search world info entries by a search term
* @param {*[]} data - WI items data array
* @param {string} searchValue - The search term
* @returns {{item?: *, refIndex: number, score: number}[]} Results as items with their score
*/
export function fuzzySearchWorldInfo(data, searchValue) {
// @ts-ignore
const fuse = new Fuse(data, {
keys: [
{ name: 'key', weight: 3 },
@ -1901,11 +1909,12 @@ export function fuzzySearchWorldInfo(data, searchValue) {
const results = fuse.search(searchValue);
console.debug('World Info fuzzy search results for ' + searchValue, results);
return results.map(x => x.item?.uid);
return results;
}
export function fuzzySearchPersonas(data, searchValue) {
data = data.map(x => ({ key: x, description: power_user.persona_descriptions[x]?.description ?? '', name: power_user.personas[x] ?? '' }));
// @ts-ignore
const fuse = new Fuse(data, {
keys: [
{ name: 'name', weight: 4 },
@ -1922,6 +1931,7 @@ export function fuzzySearchPersonas(data, searchValue) {
}
export function fuzzySearchTags(searchValue) {
// @ts-ignore
const fuse = new Fuse(tags, {
keys: [
{ name: 'name', weight: 1 },
@ -1938,6 +1948,7 @@ export function fuzzySearchTags(searchValue) {
}
export function fuzzySearchGroups(searchValue) {
// @ts-ignore
const fuse = new Fuse(groups, {
keys: [
{ name: 'name', weight: 3 },
@ -2745,6 +2756,7 @@ function setAvgBG() {
}
async function setThemeCallback(_, text) {
// @ts-ignore
const fuse = new Fuse(themes, {
keys: [
{ name: 'name', weight: 1 },
@ -2767,6 +2779,7 @@ async function setThemeCallback(_, text) {
}
async function setmovingUIPreset(_, text) {
// @ts-ignore
const fuse = new Fuse(movingUIPresets, {
keys: [
{ name: 'name', weight: 1 },

View File

@ -678,7 +678,17 @@ function sortEntries(data) {
const sortRule = option.data('rule');
const orderSign = sortOrder === 'asc' ? 1 : -1;
if (sortRule === 'custom') {
if (!data.length) return data;
// If we have a search term for WI, we are sorting by weighting scores
if ('search') {
data.sort((a, b) => {
const aScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, a.uid);
const bScore = worldInfoFilter.getScore(FILTER_TYPES.WORLD_INFO_SEARCH, b.uid);
return (aScore - bScore);
});
}
else if (sortRule === 'custom') {
// First by display index, then by order, then by uid
data.sort((a, b) => {
const aValue = a.displayIndex;
@ -756,6 +766,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return;
}
// Before printing the WI, we check if we should enable/disable search sorting
verifySearchSortRule();
function getDataArray(callback) {
// Convert the data.entries object into an array
let entriesArray = Object.keys(data.entries).map(uid => {
@ -764,10 +777,11 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
return entry;
});
// Sort the entries array by displayIndex and uid
entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);
entriesArray = sortEntries(entriesArray);
// Apply the filter and do the chosen sorting
entriesArray = worldInfoFilter.applyFilters(entriesArray);
entriesArray = sortEntries(entriesArray)
// Run the callback for printing this
typeof callback === 'function' && callback(entriesArray);
return entriesArray;
}
@ -996,6 +1010,26 @@ const originalDataKeyMap = {
'groupOverride': 'extensions.group_override',
};
/** Checks the state of the current search, and adds/removes the search sorting option accordingly */
function verifySearchSortRule() {
const searchTerm = worldInfoFilter.getFilterData(FILTER_TYPES.WORLD_INFO_SEARCH);
const searchOption = $('#world_info_sort_order option[data-rule="search"]');
const selector = $('#world_info_sort_order');
const isHidden = searchOption.attr('hidden') !== undefined;
// If we have a search term for WI, we are displaying the sorting option for it
if (searchTerm && isHidden) {
searchOption.removeAttr('hidden');
selector.val(searchOption.attr('value') || '0');
flashHighlight(selector);
}
// If search got cleared, we make sure to hide the option and go back to the one before
if (!searchTerm && !isHidden) {
searchOption.attr('hidden', '');
selector.val(localStorage.getItem(SORT_ORDER_KEY) || '0');
}
}
function setOriginalDataValue(data, uid, key, value) {
if (data.originalData && Array.isArray(data.originalData.entries)) {
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
@ -3053,7 +3087,10 @@ jQuery(() => {
$('#world_info_sort_order').on('change', function () {
const value = String($(this).find(':selected').val());
localStorage.setItem(SORT_ORDER_KEY, value);
// Save sort order, but do not save search sorting, as this is a temporary sorting option
if (value !== 'search') {
localStorage.setItem(SORT_ORDER_KEY, value);
}
updateEditor(navigation_option.none);
});