mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Scored search sorting for world info
This commit is contained in:
@ -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>
|
<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...">
|
<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">
|
<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="priority" value="0" data-i18n="Priority">Priority</option>
|
||||||
<option data-rule="custom" value="13" data-i18n="Custom">Custom</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>
|
<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;
|
const subCount = subEntities.length;
|
||||||
subEntities = filterByTagState(entities, { subForEntity: entity });
|
subEntities = filterByTagState(entities, { subForEntity: entity });
|
||||||
if (doFilter) {
|
if (doFilter) {
|
||||||
subEntities = entitiesFilter.applyFilters(subEntities);
|
subEntities = entitiesFilter.applyFilters(subEntities, { clearScoreCache: false });
|
||||||
}
|
}
|
||||||
entity.entities = subEntities;
|
entity.entities = subEntities;
|
||||||
entity.hidden = subCount - subEntities.length;
|
entity.hidden = subCount - subEntities.length;
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js';
|
import { fuzzySearchCharacters, fuzzySearchGroups, fuzzySearchPersonas, fuzzySearchTags, fuzzySearchWorldInfo, power_user } from './power-user.js';
|
||||||
import { tag_map } from './tags.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
|
* 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 = {
|
export const FILTER_TYPES = {
|
||||||
SEARCH: 'search',
|
SEARCH: 'search',
|
||||||
@ -56,12 +62,22 @@ export function isFilterState(a, b) {
|
|||||||
* data = filterHelper.applyFilters(data);
|
* data = filterHelper.applyFilters(data);
|
||||||
*/
|
*/
|
||||||
export class FilterHelper {
|
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
|
* Creates a new FilterHelper
|
||||||
* @param {Function} onDataChanged Callback to trigger when the filter data changes
|
* @param {Function} onDataChanged Callback to trigger when the filter data changes
|
||||||
*/
|
*/
|
||||||
constructor(onDataChanged) {
|
constructor(onDataChanged) {
|
||||||
this.onDataChanged = onDataChanged;
|
this.onDataChanged = onDataChanged;
|
||||||
|
this.scoreCache = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -135,7 +151,12 @@ export class FilterHelper {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const fuzzySearchResults = fuzzySearchWorldInfo(data, term);
|
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.
|
* 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) {
|
getFilterData(filterType) {
|
||||||
return this.filterData[filterType];
|
return this.filterData[filterType];
|
||||||
@ -318,11 +339,51 @@ export class FilterHelper {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies all filters to the given data.
|
* 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.
|
* @returns {any[]} The filtered data.
|
||||||
*/
|
*/
|
||||||
applyFilters(data) {
|
applyFilters(data, { clearScoreCache = true } = {}) {
|
||||||
|
if (clearScoreCache) this.clearScoreCache();
|
||||||
return Object.values(this.filterFunctions)
|
return Object.values(this.filterFunctions)
|
||||||
.reduce((data, fn) => fn(data), data);
|
.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) {
|
export function fuzzySearchCharacters(searchValue) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(characters, {
|
const fuse = new Fuse(characters, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'data.name', weight: 8 },
|
{ name: 'data.name', weight: 8 },
|
||||||
@ -1885,7 +1886,14 @@ export function fuzzySearchCharacters(searchValue) {
|
|||||||
return indices;
|
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) {
|
export function fuzzySearchWorldInfo(data, searchValue) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(data, {
|
const fuse = new Fuse(data, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'key', weight: 3 },
|
{ name: 'key', weight: 3 },
|
||||||
@ -1901,11 +1909,12 @@ export function fuzzySearchWorldInfo(data, searchValue) {
|
|||||||
|
|
||||||
const results = fuse.search(searchValue);
|
const results = fuse.search(searchValue);
|
||||||
console.debug('World Info fuzzy search results for ' + searchValue, results);
|
console.debug('World Info fuzzy search results for ' + searchValue, results);
|
||||||
return results.map(x => x.item?.uid);
|
return results;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function fuzzySearchPersonas(data, searchValue) {
|
export function fuzzySearchPersonas(data, searchValue) {
|
||||||
data = data.map(x => ({ key: x, description: power_user.persona_descriptions[x]?.description ?? '', name: power_user.personas[x] ?? '' }));
|
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, {
|
const fuse = new Fuse(data, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'name', weight: 4 },
|
{ name: 'name', weight: 4 },
|
||||||
@ -1922,6 +1931,7 @@ export function fuzzySearchPersonas(data, searchValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fuzzySearchTags(searchValue) {
|
export function fuzzySearchTags(searchValue) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(tags, {
|
const fuse = new Fuse(tags, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'name', weight: 1 },
|
{ name: 'name', weight: 1 },
|
||||||
@ -1938,6 +1948,7 @@ export function fuzzySearchTags(searchValue) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function fuzzySearchGroups(searchValue) {
|
export function fuzzySearchGroups(searchValue) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(groups, {
|
const fuse = new Fuse(groups, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'name', weight: 3 },
|
{ name: 'name', weight: 3 },
|
||||||
@ -2745,6 +2756,7 @@ function setAvgBG() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setThemeCallback(_, text) {
|
async function setThemeCallback(_, text) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(themes, {
|
const fuse = new Fuse(themes, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'name', weight: 1 },
|
{ name: 'name', weight: 1 },
|
||||||
@ -2767,6 +2779,7 @@ async function setThemeCallback(_, text) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function setmovingUIPreset(_, text) {
|
async function setmovingUIPreset(_, text) {
|
||||||
|
// @ts-ignore
|
||||||
const fuse = new Fuse(movingUIPresets, {
|
const fuse = new Fuse(movingUIPresets, {
|
||||||
keys: [
|
keys: [
|
||||||
{ name: 'name', weight: 1 },
|
{ name: 'name', weight: 1 },
|
||||||
|
@ -678,7 +678,17 @@ function sortEntries(data) {
|
|||||||
const sortRule = option.data('rule');
|
const sortRule = option.data('rule');
|
||||||
const orderSign = sortOrder === 'asc' ? 1 : -1;
|
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
|
// First by display index, then by order, then by uid
|
||||||
data.sort((a, b) => {
|
data.sort((a, b) => {
|
||||||
const aValue = a.displayIndex;
|
const aValue = a.displayIndex;
|
||||||
@ -756,6 +766,9 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before printing the WI, we check if we should enable/disable search sorting
|
||||||
|
verifySearchSortRule();
|
||||||
|
|
||||||
function getDataArray(callback) {
|
function getDataArray(callback) {
|
||||||
// Convert the data.entries object into an array
|
// Convert the data.entries object into an array
|
||||||
let entriesArray = Object.keys(data.entries).map(uid => {
|
let entriesArray = Object.keys(data.entries).map(uid => {
|
||||||
@ -764,10 +777,11 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
|
|||||||
return entry;
|
return entry;
|
||||||
});
|
});
|
||||||
|
|
||||||
// Sort the entries array by displayIndex and uid
|
// Apply the filter and do the chosen sorting
|
||||||
entriesArray.sort((a, b) => a.displayIndex - b.displayIndex || a.uid - b.uid);
|
|
||||||
entriesArray = sortEntries(entriesArray);
|
|
||||||
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
entriesArray = worldInfoFilter.applyFilters(entriesArray);
|
||||||
|
entriesArray = sortEntries(entriesArray)
|
||||||
|
|
||||||
|
// Run the callback for printing this
|
||||||
typeof callback === 'function' && callback(entriesArray);
|
typeof callback === 'function' && callback(entriesArray);
|
||||||
return entriesArray;
|
return entriesArray;
|
||||||
}
|
}
|
||||||
@ -996,6 +1010,26 @@ const originalDataKeyMap = {
|
|||||||
'groupOverride': 'extensions.group_override',
|
'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) {
|
function setOriginalDataValue(data, uid, key, value) {
|
||||||
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
if (data.originalData && Array.isArray(data.originalData.entries)) {
|
||||||
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
|
let originalEntry = data.originalData.entries.find(x => x.uid === uid);
|
||||||
@ -3053,7 +3087,10 @@ jQuery(() => {
|
|||||||
|
|
||||||
$('#world_info_sort_order').on('change', function () {
|
$('#world_info_sort_order').on('change', function () {
|
||||||
const value = String($(this).find(':selected').val());
|
const value = String($(this).find(':selected').val());
|
||||||
|
// 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);
|
localStorage.setItem(SORT_ORDER_KEY, value);
|
||||||
|
}
|
||||||
updateEditor(navigation_option.none);
|
updateEditor(navigation_option.none);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user