Scored search sorting for world info
This commit is contained in:
parent
e4de6da5b8
commit
a850352eab
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 },
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
||||
|
|
Loading…
Reference in New Issue