Merge pull request #2152 from Wolfsblvt/auto-sort-tags-option

Option to auto-sort tags (+UI improvements)
This commit is contained in:
Cohee 2024-04-27 17:45:23 +03:00 committed by GitHub
commit f479901c87
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 166 additions and 94 deletions

View File

@ -152,6 +152,7 @@ import {
Stopwatch,
isValidUrl,
ensureImageFormatSupported,
flashHighlight,
} from './scripts/utils.js';
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, renderExtensionTemplate, renderExtensionTemplateAsync, runGenerationInterceptors, saveMetadataDebounced, writeExtensionField } from './scripts/extensions.js';
@ -6799,10 +6800,7 @@ function select_rm_info(type, charId, previousCharId = null) {
const scrollOffset = element.offset().top - element.parent().offset().top;
element.parent().scrollTop(scrollOffset);
element.addClass('flash animated');
setTimeout(function () {
element.removeClass('flash animated');
}, 5000);
flashHighlight(element, 5000);
});
} catch (e) {
console.error(e);
@ -6828,10 +6826,7 @@ function select_rm_info(type, charId, previousCharId = null) {
const element = $(selector);
const scrollOffset = element.offset().top - element.parent().offset().top;
element.parent().scrollTop(scrollOffset);
$(element).addClass('flash animated');
setTimeout(function () {
$(element).removeClass('flash animated');
}, 5000);
flashHighlight(element, 5000);
});
} catch (e) {
console.error(e);
@ -7100,57 +7095,49 @@ function onScenarioOverrideRemoveClick() {
* @returns
*/
function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, allowHorizontalScrolling, allowVerticalScrolling, cropAspect } = {}) {
function getOkButtonText() {
if (['avatarToCrop'].includes(popup_type)) {
return okButton ?? 'Accept';
} else if (['text', 'alternate_greeting', 'char_not_selected'].includes(popup_type)) {
$dialoguePopupCancel.css('display', 'none');
return okButton ?? 'Ok';
} else if (['delete_extension'].includes(popup_type)) {
return okButton ?? 'Ok';
} else if (['new_chat', 'confirm'].includes(popup_type)) {
return okButton ?? 'Yes';
} else if (['input'].includes(popup_type)) {
return okButton ?? 'Save';
}
return okButton ?? 'Delete';
}
dialogueCloseStop = true;
if (type) {
popup_type = type;
}
$('#dialogue_popup').toggleClass('wide_dialogue_popup', !!wide);
$('#dialogue_popup').toggleClass('large_dialogue_popup', !!large);
$('#dialogue_popup').toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling);
$('#dialogue_popup').toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
const $dialoguePopup = $('#dialogue_popup');
const $dialoguePopupCancel = $('#dialogue_popup_cancel');
const $dialoguePopupOk = $('#dialogue_popup_ok');
const $dialoguePopupInput = $('#dialogue_popup_input');
const $dialoguePopupText = $('#dialogue_popup_text');
const $shadowPopup = $('#shadow_popup');
$('#dialogue_popup_cancel').css('display', 'inline-block');
switch (popup_type) {
case 'avatarToCrop':
$('#dialogue_popup_ok').text(okButton ?? 'Accept');
break;
case 'text':
case 'alternate_greeting':
case 'char_not_selected':
$('#dialogue_popup_ok').text(okButton ?? 'Ok');
$('#dialogue_popup_cancel').css('display', 'none');
break;
case 'delete_extension':
$('#dialogue_popup_ok').text(okButton ?? 'Ok');
break;
case 'new_chat':
case 'confirm':
$('#dialogue_popup_ok').text(okButton ?? 'Yes');
break;
case 'del_group':
case 'rename_chat':
case 'del_chat':
default:
$('#dialogue_popup_ok').text(okButton ?? 'Delete');
}
$dialoguePopup.toggleClass('wide_dialogue_popup', !!wide)
.toggleClass('large_dialogue_popup', !!large)
.toggleClass('horizontal_scrolling_dialogue_popup', !!allowHorizontalScrolling)
.toggleClass('vertical_scrolling_dialogue_popup', !!allowVerticalScrolling);
$('#dialogue_popup_input').val(inputValue);
$('#dialogue_popup_input').attr('rows', rows ?? 1);
$dialoguePopupCancel.css('display', 'inline-block');
$dialoguePopupOk.text(getOkButtonText());
$dialoguePopupInput.toggle(popup_type === 'input').val(inputValue).attr('rows', rows ?? 1);
$dialoguePopupText.empty().append(text);
$shadowPopup.css('display', 'block');
if (popup_type == 'input') {
$('#dialogue_popup_input').css('display', 'block');
$('#dialogue_popup_ok').text(okButton ?? 'Save');
}
else {
$('#dialogue_popup_input').css('display', 'none');
$dialoguePopupInput.trigger('focus');
}
$('#dialogue_popup_text').empty().append(text);
$('#shadow_popup').css('display', 'block');
if (popup_type == 'input') {
$('#dialogue_popup_input').focus();
}
if (popup_type == 'avatarToCrop') {
// unset existing data
crop_data = undefined;
@ -7166,7 +7153,8 @@ function callPopup(text, type, inputValue = '', { okButton, rows, wide, large, a
},
});
}
$('#shadow_popup').transition({
$shadowPopup.transition({
opacity: 1,
duration: animation_duration,
easing: animation_easing,

View File

@ -1,7 +1,7 @@
import { callPopup, chat_metadata, eventSource, event_types, generateQuietPrompt, getCurrentChatId, getRequestHeaders, getThumbnailUrl, saveSettingsDebounced } from '../script.js';
import { saveMetadataDebounced } from './extensions.js';
import { registerSlashCommand } from './slash-commands.js';
import { stringFormat } from './utils.js';
import { flashHighlight, stringFormat } from './utils.js';
const BG_METADATA_KEY = 'custom_background';
const LIST_METADATA_KEY = 'chat_backgrounds';
@ -453,8 +453,7 @@ function highlightNewBackground(bg) {
const newBg = $(`.bg_example[bgfile="${bg}"]`);
const scrollOffset = newBg.offset().top - newBg.parent().offset().top;
$('#Backgrounds').scrollTop(scrollOffset);
newBg.addClass('flash animated');
setTimeout(() => newBg.removeClass('flash animated'), 2000);
flashHighlight(newBg);
}
function onBackgroundFilterInput() {

View File

@ -15,7 +15,7 @@ import {
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay } from './utils.js';
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
import { power_user } from './power-user.js';
export {
@ -350,18 +350,20 @@ function createTagMapFromList(listElement, key) {
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
*
* @param {string} key - The key for which to get tags via the tag map
* @param {boolean} [sort=true] -
* @returns {Tag[]} A list of tags
*/
function getTagsList(key) {
function getTagsList(key, sort = true) {
if (!Array.isArray(tag_map[key])) {
tag_map[key] = [];
return [];
}
return tag_map[key]
const list = tag_map[key]
.map(x => tags.find(y => y.id === x))
.filter(x => x)
.sort(compareTagsForSort);
.filter(x => x);
if (sort) list.sort(compareTagsForSort);
return list;
}
function getInlineListSelector() {
@ -644,6 +646,7 @@ function createNewTag(tagName) {
* @property {Tag} [addTag=undefined] - Optionally provide a tag that should be manually added to this print. Either to the overriden tag list or the found tags based on the entity/key. Will respect the tag exists check.
* @property {object|number|string} [forEntityOrKey=undefined] - Optional override for the chosen entity, otherwise the currently selected is chosen. Can be an entity with id property (character, group, tag), or directly an id or tag key.
* @property {boolean|string} [empty=true] - Whether the list should be initially empty. If a string string is provided, 'always' will always empty the list, otherwise it'll evaluate to a boolean.
* @property {boolean} [sort=true] - Whether the tags should be sorted via the sort function, or kept as is.
* @property {function(object): function} [tagActionSelector=undefined] - An optional override for the action property that can be assigned to each tag via tagOptions.
* If set, the selector is executed on each tag as input argument. This allows a list of tags to be provided and each tag can have it's action based on the tag object itself.
* @property {TagOptions} [tagOptions={}] - Options for tag behavior. (Same object will be passed into "appendTagToList")
@ -655,10 +658,10 @@ function createNewTag(tagName) {
* @param {JQuery<HTMLElement>|string} element - The container element where the tags are to be printed. (Optionally can also be a string selector for the element, which will then be resolved)
* @param {PrintTagListOptions} [options] - Optional parameters for printing the tag list.
*/
function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
function printTagList(element, { tags = undefined, addTag = undefined, forEntityOrKey = undefined, empty = true, sort = true, tagActionSelector = undefined, tagOptions = {} } = {}) {
const $element = (typeof element === 'string') ? $(element) : element;
const key = forEntityOrKey !== undefined ? getTagKeyForEntity(forEntityOrKey) : getTagKey();
let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key);
let printableTags = tags ? (typeof tags === 'function' ? tags() : tags) : getTagsList(key, sort);
if (empty === 'always' || (empty && (printableTags?.length > 0 || key))) {
$element.empty();
@ -669,7 +672,7 @@ function printTagList(element, { tags = undefined, addTag = undefined, forEntity
}
// one last sort, because we might have modified the tag list or manually retrieved it from a function
printableTags = printableTags.sort(compareTagsForSort);
if (sort) printableTags = printableTags.sort(compareTagsForSort);
const customAction = typeof tagActionSelector === 'function' ? tagActionSelector : null;
@ -872,10 +875,10 @@ function printTagFilters(type = tag_filter_types.character) {
// Print all action tags. (Exclude folder if that setting isn't chosen)
const actionTags = Object.values(ACTIONABLE_TAGS).filter(tag => power_user.bogus_folders || tag.id != ACTIONABLE_TAGS.FOLDER.id);
printTagList($(FILTER_SELECTOR), { empty: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: actionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
const inListActionTags = Object.values(InListActionable);
printTagList($(FILTER_SELECTOR), { empty: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
printTagList($(FILTER_SELECTOR), { empty: false, sort: false, tags: inListActionTags, tagActionSelector: tag => tag.action, tagOptions: { isGeneralList: true } });
const characterTagIds = Object.values(tag_map).flat();
const tagsToDisplay = tags.filter(x => characterTagIds.includes(x.id)).sort(compareTagsForSort);
@ -992,11 +995,11 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
}
function onViewTagsListClick() {
$('#dialogue_popup').addClass('large_dialogue_popup');
const list = $(document.createElement('div'));
list.attr('id', 'tag_view_list');
const everything = Object.values(tag_map).flat();
$(list).append(`
const popup = $('#dialogue_popup');
popup.addClass('large_dialogue_popup');
const html = $(document.createElement('div'));
html.attr('id', 'tag_view_list');
html.append(`
<div class="title_restorable alignItemsBaseline">
<h3>Tag Management</h3>
<div class="flex-container alignItemsBaseline">
@ -1017,25 +1020,57 @@ function onViewTagsListClick() {
</div>
<div class="justifyLeft m-b-1">
<small>
Drag the handle to reorder.<br>
Drag handle to reorder. Click name to rename. Click color to change display.<br>
${(power_user.bogus_folders ? 'Click on the folder icon to use this tag as a folder.<br>' : '')}
Click on the tag name to edit it.<br>
Click on color box to assign new color.
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-1" for="auto_sort_tags">
<input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" ${power_user.auto_sort_tags ? ' checked' : ''} />
<span data-i18n="Use alphabetical sorting">
Use alphabetical sorting
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf disabled, new tags will be appended at the end.\n\nIf a tag is manually reordered by dragging, automatic sorting will be disabled."
title="If enabled, tags will automatically be sorted alphabetically on creation or rename.\nIf disabled, new tags will be appended at the end.\n\nIf a tag is manually reordered by dragging, automatic sorting will be disabled.">
</div>
</span>
</label>
</small>
</div>`);
const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
list.append(tagContainer);
html.append(tagContainer);
const sortedTags = sortTags(tags);
for (const tag of sortedTags) {
appendViewTagToList(tagContainer, tag, everything);
}
callPopup(html, 'text', null, { allowVerticalScrolling: true });
printViewTagList();
makeTagListDraggable(tagContainer);
callPopup(list, 'text');
$('#dialogue_popup .tag-color').on('change', (evt) => onTagColorize(evt));
$('#dialogue_popup .tag-color2').on('change', (evt) => onTagColorize2(evt));
}
/**
* Print the list of tags in the tag management view
* @param {Event} event Event that triggered the color change
* @param {boolean} toggle State of the toggle
*/
function toggleAutoSortTags(event, toggle) {
if (toggle === power_user.auto_sort_tags) return;
// Ask user to confirm if enabling and it was manually sorted before
if (toggle && isManuallySorted() && !confirm('Are you sure you want to automatically sort alphabetically?')) {
if (event.target instanceof HTMLInputElement) {
event.target.checked = false;
}
return;
}
power_user.auto_sort_tags = toggle;
printCharactersDebounced();
saveSettingsDebounced();
}
/** This function goes over all existing tags and checks whether they were reorderd in the past. @returns {boolean} */
function isManuallySorted() {
return tags.some((tag, index) => tag.sort_order !== index);
}
function makeTagListDraggable(tagContainer) {
@ -1067,6 +1102,13 @@ function makeTagListDraggable(tagContainer) {
}
});
// If tags were dragged manually, we have to disable auto sorting
if (power_user.auto_sort_tags) {
power_user.auto_sort_tags = false;
$('#dialogue_popup input[name="auto_sort_tags"]').prop('checked', false);
toastr.info('Automatic sorting of tags deactivated.');
}
// If the order of tags in display has changed, we need to redraw some UI elements. Do it debounced so it doesn't block and you can drag multiple tags.
printCharactersDebounced();
saveSettingsDebounced();
@ -1098,6 +1140,11 @@ function sortTags(tags) {
* @returns {number} The compare result
*/
function compareTagsForSort(a, b) {
const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
if (power_user.auto_sort_tags) {
return defaultSort;
}
if (a.sort_order !== undefined && b.sort_order !== undefined) {
return a.sort_order - b.sort_order;
} else if (a.sort_order !== undefined) {
@ -1105,7 +1152,7 @@ function compareTagsForSort(a, b) {
} else if (b.sort_order !== undefined) {
return 1;
} else {
return a.name.toLowerCase().localeCompare(b.name.toLowerCase());
return defaultSort;
}
}
@ -1208,7 +1255,10 @@ function onTagsBackupClick() {
function onTagCreateClick() {
const tag = createNewTag('New Tag');
appendViewTagToList($('#tag_view_list .tag_view_list_tags'), tag, []);
printViewTagList();
const tagElement = ($('#dialogue_popup .tag_view_list_tags')).find(`.tag_view_item[id="${tag.id}"]`);
flashHighlight(tagElement);
printCharactersDebounced();
saveSettingsDebounced();
@ -1248,18 +1298,6 @@ function appendViewTagToList(list, tag, everything) {
list.append(template);
setTimeout(function () {
document.querySelector(`.tag-color[id="${colorPickerId}"`).addEventListener('change', (evt) => {
onTagColorize(evt);
});
}, 100);
setTimeout(function () {
document.querySelector(`.tag-color2[id="${colorPicker2Id}"`).addEventListener('change', (evt) => {
onTagColorize2(evt);
});
}, 100);
updateDrawTagFolder(template, tag);
// @ts-ignore
@ -1394,6 +1432,17 @@ function copyTags(data) {
tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
}
function printViewTagList(empty = true) {
const tagContainer = $('#dialogue_popup .tag_view_list_tags');
if (empty) tagContainer.empty();
const everything = Object.values(tag_map).flat();
const sortedTags = sortTags(tags);
for (const tag of sortedTags) {
appendViewTagToList(tagContainer, tag, everything);
}
}
export function initTags() {
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
@ -1412,4 +1461,31 @@ export function initTags() {
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
const toggle = $(evt.target).is(':checked');
toggleAutoSortTags(evt.originalEvent, toggle);
printViewTagList();
});
$(document).on('focusout', `#dialogue_popup .tag_view_name`, (evt) => {
// Remember the order, so we can flash highlight if it changed after reprinting
const tagId = $(evt.target).parent('.tag_view_item').attr('id');
const oldOrder = $(`#dialogue_popup .tag_view_item`).map((_, el) => el.id).get();
printViewTagList();
const newOrder = $(`#dialogue_popup .tag_view_item`).map((_, el) => el.id).get();
const orderChanged = !oldOrder.every((id, index) => id === newOrder[index]);
if (orderChanged) {
flashHighlight($(`#dialogue_popup .tag_view_item[id="${tagId}"]`));
}
});
// Initialize auto sort setting based on whether it was sorted before
if (power_user.auto_sort_tags === undefined || power_user.auto_sort_tags === null) {
power_user.auto_sort_tags = !isManuallySorted();
if (power_user.auto_sort_tags) {
printCharactersDebounced();
}
}
}

View File

@ -1419,3 +1419,13 @@ export function setValueByPath(obj, path, value) {
currentObject[keyParts[keyParts.length - 1]] = value;
}
/**
* Flashes the given HTML element via CSS flash animation for a defined period
* @param {JQuery<HTMLElement>} element - The element to flash
* @param {number} timespan - A numer in milliseconds how the flash should last
*/
export function flashHighlight(element, timespan = 2000) {
element.addClass('flash animated');
setTimeout(() => element.removeClass('flash animated'), timespan);
}

View File

@ -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 } from './utils.js';
import { download, debounce, initScrollHeight, resetScrollHeight, parseJsonFile, extractDataFromPng, getFileBuffer, getCharaFilename, getSortableDelay, escapeRegex, PAGINATION_TEMPLATE, navigation_option, waitUntilCondition, isTrueBoolean, setValueByPath, flashHighlight } 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';
@ -854,8 +854,7 @@ function displayWorldEntries(name, data, navigation = navigation_option.none) {
const parentOffset = element.parent().offset();
const scrollOffset = elementOffset.top - parentOffset.top;
$('#WorldInfo').scrollTop(scrollOffset);
element.addClass('flash animated');
setTimeout(() => element.removeClass('flash animated'), 2000);
flashHighlight(element);
});
}