Feature: allows sorting tags by most used (#4768) * add sorting tags by most used * Fix whitespaces * Code review updates * Remove commented code * Fix capitalization in comment * Apply review suggestion * Simplify template init * Reformat * Add documentation for appendViewTagToList and printViewTagList functions * Reprint renamed tags regardless of sorting mode --------- Co-authored-by: Cohee <18619528+Cohee1207@users.noreply.github.com>

e7af479427601a6affe58ab9bf1373cd5464bdfb

Serena <85121347+AzureKarasuba@users.noreply.github.com>

Signed
5 files changed, +74 -62Ignore whitespace
public/locales/zh-cn.json+4 -0
@@ -2272,6 +2272,10 @@
22722272 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。",
22732273 "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。",
22742274 "Use alphabetical sorting": "按字母顺序排列",
2275+ "Sort mode": "排序模式",
2276+ "Manual (Drag & Drop)": "手动 (拖放)",
2277+ "Alphabetical (A-Z)": "按字母 (A-Z)",
2278+ "Most Used (By Count)": "按使用次数",
22752279 "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。",
22762280 "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?",
22772281 "Hi,": "嗨,",
public/locales/zh-tw.json+4 -0
@@ -2107,6 +2107,10 @@
21072107 "Drag handle to reorder. Click name to rename. Click color to change display.": "拖動以重新排序。點選名稱重新命名。點選顏色更改顯示。",
21082108 "Click on the folder icon to use this tag as a folder.": "點選資料夾圖示以將此標籤作為資料夾。",
21092109 "Use alphabetical sorting": "按字母順序排序 ",
2110+ "Sort mode": "排序模式",
2111+ "Manual (Drag & Drop)": "手動(拖放)",
2112+ "Alphabetical (A-Z)": "按字母(A-Z)",
2113+ "Most Used (By Count)": "按使用次數",
21102114 "tags_sorting_desc": "啟用後,標籤將在建立或重新命名時將自動按字母排序。\n停用時,新標籤將附加到結尾。\n若標籤被手動拖動重新排序,則自動排序將被停用。",
21112115 "and connect to an": "並連線到",
21122116 "You can add more": "您可加入更多",
public/scripts/power-user.js+6 -1
@@ -44,7 +44,7 @@ import {
4444 updateBindModelTemplatesState,
4545} from './instruct-mode.js';
4646
4747import { getTagsList, tag_import_setting, tag_map, tag_sort_mode, tags } from './tags.js';
4848import { tokenizers } from './tokenizers.js';
4949import { BIAS_CACHE } from './logit-bias.js';
5050import { renderTemplateAsync } from './templates.js';
@@ -216,6 +216,7 @@ export const power_user = {
216216 enable_auto_select_input: false,
217217 enable_md_hotkeys: false,
218218 tag_import_setting: tag_import_setting.ASK,
219+ tag_sort_mode: tag_sort_mode.MANUAL,
219220 disable_group_trimming: false,
220221 single_line: false,
221222
@@ -1556,6 +1557,10 @@ export async function loadPowerUserSettings(settings, data) {
15561557 if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) {
15571558 settings.power_user.click_to_edit = true;
15581559 }
1560+ if (Object.hasOwn(settings.power_user, 'auto_sort_tags') && !Object.hasOwn(settings.power_user, 'tag_sort_mode')) {
1561+ settings.power_user.tag_sort_mode = settings.power_user.auto_sort_tags ? tag_sort_mode.ALPHABETICAL : tag_sort_mode.MANUAL;
1562+ delete settings.power_user.auto_sort_tags;
1563+ }
15591564 Object.assign(power_user, settings.power_user);
15601565 }
15611566
public/scripts/tags.js+52 -52
@@ -85,6 +85,13 @@ export const tag_import_setting = {
8585 ONLY_EXISTING: 4,
8686};
8787
88+/** @enum {string} */
89+export const tag_sort_mode = {
90+ MANUAL: 'manual',
91+ ALPHABETICAL: 'alphabetical',
92+ BY_ENTRIES: 'by_entries',
93+};
94+
8895/**
8996 * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }}
9097 * A collection of global actional tags for the filter panel
@@ -140,6 +147,7 @@ const TAG_FOLDER_DEFAULT_TYPE = 'NONE';
140147 * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`)
141148 * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`)
142149 * @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted
150+ * @property {number} [count] - The number of entities that have this tag assigned
143151 * @property {string} [color] - The background color of the tag
144152 * @property {string} [color2] - The foreground color of the tag
145153 * @property {number} [create_date] - A number representing the date when this tag was created
@@ -1331,44 +1339,26 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {})
13311339async function onViewTagsListClick() {
13321340 const html = $(document.createElement('div'));
13331341 html.attr('id', 'tag_view_list');
13341342 html.append(await renderTemplateAsync('tagManagement', { bogus_folders: power_user.bogus_folders, auto_sort_tags: power_user.auto_sort_tags }));
13351343
13361344 const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>');
13371345 html.append(tagContainer);
13381346
1347+ const $sortModeSelect = html.find('#tag_sort_mode_select');
1348+ $sortModeSelect.val(power_user.tag_sort_mode);
1349+ $sortModeSelect.on('change', function () {
1350+ const newMode = $(this).val().toString();
1351+ power_user.tag_sort_mode = newMode;
1352+ saveSettingsDebounced();
1353+ printViewTagList(tagContainer);
1354+ });
1355+
13391356 printViewTagList(tagContainer);
13401357 makeTagListDraggable(tagContainer);
13411358
13421359 await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, wide: true, large: true });
13431360}
13441361
1345-/**
1346- * Print the list of tags in the tag management view
1347- * @param {Event} event Event that triggered the color change
1348- * @param {boolean} toggle State of the toggle
1349- */
1350-function toggleAutoSortTags(event, toggle) {
1351- if (toggle === power_user.auto_sort_tags) return;
1352-
1353- // Ask user to confirm if enabling and it was manually sorted before
1354- if (toggle && isManuallySorted() && !confirm('Are you sure you want to automatically sort alphabetically?')) {
1355- if (event.target instanceof HTMLInputElement) {
1356- event.target.checked = false;
1357- }
1358- return;
1359- }
1360-
1361- power_user.auto_sort_tags = toggle;
1362-
1363- printCharactersDebounced();
1364- saveSettingsDebounced();
1365-}
1366-
1367-/** This function goes over all existing tags and checks whether they were reorderd in the past. @returns {boolean} */
1368-function isManuallySorted() {
1369- return tags.some((tag, index) => tag.sort_order !== index);
1370-}
1371-
13721362function makeTagListDraggable(tagContainer) {
13731363 const onTagsSort = () => {
13741364 tagContainer.find('.tag_view_item').each(function (i, tagElement) {
@@ -1380,10 +1370,10 @@ function makeTagListDraggable(tagContainer) {
13801370 });
13811371
13821372 // If tags were dragged manually, we have to disable auto sorting
13831373 if (power_user.auto_sort_tagstag_sort_mode !== tag_sort_mode.MANUAL) {
13841374 power_user.auto_sort_tagstag_sort_mode = falsetag_sort_mode.MANUAL;
13851375 $('#tag_view_list input[name="auto_sort_tags"]tag_sort_mode_select').propval('checked', falsetag_sort_mode.MANUAL);
13861376 toastr.info('AutomaticSwitched sortingto ofManual tagssorting deactivatedmode.');
13871377 }
13881378
13891379 // 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.
@@ -1417,11 +1407,20 @@ function sortTags(tags) {
14171407 * @returns {number} The compare result
14181408 */
14191409function compareTagsForSort(a, b) {
1410+ // default sort: alphabetical, case insensitive
14201411 const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase());
1421- if (power_user.auto_sort_tags) {
1412+
1413+ // sort on number of entries
1414+ if (power_user.tag_sort_mode === tag_sort_mode.BY_ENTRIES) {
1415+ return ((b.count || 0) - (a.count || 0)) || defaultSort;
1416+ }
1417+
1418+ // alphabetical sort
1419+ if (power_user.tag_sort_mode === tag_sort_mode.ALPHABETICAL) {
14221420 return defaultSort;
14231421 }
14241422
1423+ // manual sort
14251424 if (a.sort_order !== undefined && b.sort_order !== undefined) {
14261425 return a.sort_order - b.sort_order;
14271426 } else if (a.sort_order !== undefined) {
@@ -1620,8 +1619,13 @@ function onTagCreateClick() {
16201619 toastr.success('Tag created', 'Create Tag');
16211620}
16221621
1623-function appendViewTagToList(list, tag, everything) {
1622+/**
1624- const count = everything.filter(x => x == tag.id).length;
1623+ * Appends a tag to the view tag list.
1624+ * @param {JQuery<HTMLElement>} list List element
1625+ * @param {Tag} tag Tag object
1626+ * @param {number} count Count of characters/groups using this tag
1627+ */
1628+function appendViewTagToList(list, tag, count) {
16251629 const template = VIEW_TAG_TEMPLATE.clone();
16261630 template.attr('id', tag.id);
16271631 template.find('.tag_view_counter_value').text(count);
@@ -1869,12 +1873,21 @@ function copyTags(data) {
18691873 tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap]));
18701874}
18711875
1876+/**
1877+ * Prints the tag list in the view tags popup.
1878+ * @param {JQuery<HTMLElement>} tagContainer Container element
1879+ * @param {boolean} empty Whether to empty the container before printing
1880+ */
18721881function printViewTagList(tagContainer, empty = true) {
18731882 if (empty) tagContainer.empty();
18741883 const everything = Object.values(tag_map).flat();
1875- const sortedTags = sortTags(tags);
1884+ const tagsWithCounts = tags.map(tag => {
1885+ const count = everything.filter(x => x === tag.id).length;
1886+ return { ...tag, count: count };
1887+ });
1888+ const sortedTags = sortTags(tagsWithCounts);
18761889 for (const tag of sortedTags) {
18771890 appendViewTagToList(tagContainer, tag, everythingtag.count);
18781891 }
18791892}
18801893
@@ -2296,14 +2309,9 @@ export function initTags() {
22962309 eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
22972310 eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
22982311
2299- $(document).on('input', '#tag_view_list input[name="auto_sort_tags"]', (evt) => {
2300- const toggle = $(evt.target).is(':checked');
2301- toggleAutoSortTags(evt.originalEvent, toggle);
2302- printViewTagList($('#tag_view_list .tag_view_list_tags'));
2303- });
23042312 $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => {
23052313 // Reorder/reprint tags, but only if the name actually has changed, and only if we auto sort tags
23062314 if (!power_user.auto_sort_tags || !$(evt.target).is('[dirty]')) return;
23072315
23082316 // Remember the order, so we can flash highlight if it changed after reprinting
23092317 const tagId = ($(evt.target).closest('.tag_view_item')).attr('id');
@@ -2324,14 +2332,6 @@ export function initTags() {
23242332 }
23252333 });
23262334
2327- // Initialize auto sort setting based on whether it was sorted before
2328- if (power_user.auto_sort_tags === undefined || power_user.auto_sort_tags === null) {
2329- power_user.auto_sort_tags = !isManuallySorted();
2330- if (power_user.auto_sort_tags) {
2331- printCharactersDebounced();
2332- }
2333- }
2334-
23352335 registerTagsSlashCommands();
23362336 restoreSavedTagFilters();
23372337}
public/scripts/templates/tagManagement.html+8 -9
@@ -21,17 +21,16 @@
2121 </div>
2222 </div>
2323 <div class="justifyLeft m-b-1">
24+ <div class="flex-container alignItemsBaseline">
25+ <span data-i18n="Sort mode">Sort mode:</span>
26+ <select id="tag_sort_mode_select" class="flex1 text_pole">
27+ <option value="manual" data-i18n="Manual (Drag & Drop)">Manual (Drag & Drop)</option>
28+ <option value="alphabetical" data-i18n="Alphabetical (A-Z)">Alphabetical (A-Z)</option>
29+ <option value="by_entries" data-i18n="Most Used (By Count)">Most Used (By Count)</option>
30+ </select>
31+ </div>
2432 <small>
2533 <span data-i18n="Drag handle to reorder. Click name to rename. Click color to change display.">Drag handle to reorder. Click name to rename. Click color to change display.</span><br>
2634 {{#if bogus_folders}}<span data-i18n="Click on the folder icon to use this tag as a folder.">Click on the folder icon to use this tag as a folder.</span><br>{{/if}}
27- <label class="checkbox_label flex-container alignItemsCenter m-t-1" for="auto_sort_tags">
28- <input type="checkbox" id="auto_sort_tags" name="auto_sort_tags" {{#if auto_sort_tags}} checked{{/if}} />
29- <span data-i18n="Use alphabetical sorting">
30- Use alphabetical sorting
31- </span>
32- <div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]tags_sorting_desc"
33- title="If enabled, tags will automatically be sorted alphabetically on creation or rename.&#10;If disabled, new tags will be appended at the end.&#10;&#10;If a tag is manually reordered by dragging, automatic sorting will be disabled.">
34- </div>
35- </label>
3635 </small>
3736 </div>