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>
Signed| @@ -2272,6 +2272,10 @@ | |||
| 2272 | "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。", | 2272 | "Drag handle to reorder. Click name to rename. Click color to change display.": "拖拽左侧三条横线以排序,点击名字以重命名,点击调色盘以切换颜色。", |
| 2273 | "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。", | 2273 | "Click on the folder icon to use this tag as a folder.": "点击文件夹图标来将此标签作为一个文件夹。", |
| 2274 | "Use alphabetical sorting": "按字母顺序排列", | 2274 | "Use alphabetical sorting": "按字母顺序排列", |
| 2275 | "Sort mode": "排序模式", | ||
| 2276 | "Manual (Drag & Drop)": "手动 (拖放)", | ||
| 2277 | "Alphabetical (A-Z)": "按字母 (A-Z)", | ||
| 2278 | "Most Used (By Count)": "按使用次数", | ||
| 2275 | "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。", | 2279 | "tags_sorting_desc": "启用后,标签在创建或重命名时会自动按字母顺序排序。\n禁用后,新标签会追加到末尾。\n\n如果通过拖动手动重新排列标签,则自动排序将被禁用。", |
| 2276 | "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?", | 2280 | "Are you sure you want to delete the theme?": "你确定要删除这个主题吗?", |
| 2277 | "Hi,": "嗨,", | 2281 | "Hi,": "嗨,", |
| @@ -2107,6 +2107,10 @@ | |||
| 2107 | "Drag handle to reorder. Click name to rename. Click color to change display.": "拖動以重新排序。點選名稱重新命名。點選顏色更改顯示。", | 2107 | "Drag handle to reorder. Click name to rename. Click color to change display.": "拖動以重新排序。點選名稱重新命名。點選顏色更改顯示。", |
| 2108 | "Click on the folder icon to use this tag as a folder.": "點選資料夾圖示以將此標籤作為資料夾。", | 2108 | "Click on the folder icon to use this tag as a folder.": "點選資料夾圖示以將此標籤作為資料夾。", |
| 2109 | "Use alphabetical sorting": "按字母順序排序 ", | 2109 | "Use alphabetical sorting": "按字母順序排序 ", |
| 2110 | "Sort mode": "排序模式", | ||
| 2111 | "Manual (Drag & Drop)": "手動(拖放)", | ||
| 2112 | "Alphabetical (A-Z)": "按字母(A-Z)", | ||
| 2113 | "Most Used (By Count)": "按使用次數", | ||
| 2110 | "tags_sorting_desc": "啟用後,標籤將在建立或重新命名時將自動按字母排序。\n停用時,新標籤將附加到結尾。\n若標籤被手動拖動重新排序,則自動排序將被停用。", | 2114 | "tags_sorting_desc": "啟用後,標籤將在建立或重新命名時將自動按字母排序。\n停用時,新標籤將附加到結尾。\n若標籤被手動拖動重新排序,則自動排序將被停用。", |
| 2111 | "and connect to an": "並連線到", | 2115 | "and connect to an": "並連線到", |
| 2112 | "You can add more": "您可加入更多", | 2116 | "You can add more": "您可加入更多", |
| @@ -44,7 +44,7 @@ import { | |||
| 44 | updateBindModelTemplatesState, | 44 | updateBindModelTemplatesState, |
| 45 | } from './instruct-mode.js'; | 45 | } from './instruct-mode.js'; |
| 46 | 46 | ||
| 47 | import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js'; | 47 | import { getTagsList, tag_import_setting, tag_map, tag_sort_mode, tags } from './tags.js'; |
| 48 | import { tokenizers } from './tokenizers.js'; | 48 | import { tokenizers } from './tokenizers.js'; |
| 49 | import { BIAS_CACHE } from './logit-bias.js'; | 49 | import { BIAS_CACHE } from './logit-bias.js'; |
| 50 | import { renderTemplateAsync } from './templates.js'; | 50 | import { renderTemplateAsync } from './templates.js'; |
| @@ -216,6 +216,7 @@ export const power_user = { | |||
| 216 | enable_auto_select_input: false, | 216 | enable_auto_select_input: false, |
| 217 | enable_md_hotkeys: false, | 217 | enable_md_hotkeys: false, |
| 218 | tag_import_setting: tag_import_setting.ASK, | 218 | tag_import_setting: tag_import_setting.ASK, |
| 219 | tag_sort_mode: tag_sort_mode.MANUAL, | ||
| 219 | disable_group_trimming: false, | 220 | disable_group_trimming: false, |
| 220 | single_line: false, | 221 | single_line: false, |
| 221 | 222 | ||
| @@ -1556,6 +1557,10 @@ export async function loadPowerUserSettings(settings, data) { | |||
| 1556 | if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) { | 1557 | if (settings.power_user.click_to_edit === undefined && settings.power_user.chat_display === chat_styles.DOCUMENT) { |
| 1557 | settings.power_user.click_to_edit = true; | 1558 | settings.power_user.click_to_edit = true; |
| 1558 | } | 1559 | } |
| 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 | } | ||
| 1559 | Object.assign(power_user, settings.power_user); | 1564 | Object.assign(power_user, settings.power_user); |
| 1560 | } | 1565 | } |
| 1561 | 1566 | ||
| @@ -85,6 +85,13 @@ export const tag_import_setting = { | |||
| 85 | ONLY_EXISTING: 4, | 85 | ONLY_EXISTING: 4, |
| 86 | }; | 86 | }; |
| 87 | 87 | ||
| 88 | /** @enum {string} */ | ||
| 89 | export const tag_sort_mode = { | ||
| 90 | MANUAL: 'manual', | ||
| 91 | ALPHABETICAL: 'alphabetical', | ||
| 92 | BY_ENTRIES: 'by_entries', | ||
| 93 | }; | ||
| 94 | |||
| 88 | /** | 95 | /** |
| 89 | * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }} | 96 | * @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }} |
| 90 | * A collection of global actional tags for the filter panel | 97 | * A collection of global actional tags for the filter panel |
| @@ -140,6 +147,7 @@ const TAG_FOLDER_DEFAULT_TYPE = 'NONE'; | |||
| 140 | * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`) | 147 | * @property {string} [folder_type] - The bogus folder type of this tag (based on `TAG_FOLDER_TYPES`) |
| 141 | * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`) | 148 | * @property {string} [filter_state] - The saved state of the filter chosen of this tag (based on `FILTER_STATES`) |
| 142 | * @property {number} [sort_order] - A custom integer representing the sort order if tags are sorted | 149 | * @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 | ||
| 143 | * @property {string} [color] - The background color of the tag | 151 | * @property {string} [color] - The background color of the tag |
| 144 | * @property {string} [color2] - The foreground color of the tag | 152 | * @property {string} [color2] - The foreground color of the tag |
| 145 | * @property {number} [create_date] - A number representing the date when this tag was created | 153 | * @property {number} [create_date] - A number representing the date when this tag was created |
| @@ -1331,44 +1339,26 @@ export function createTagInput(inputSelector, listSelector, tagListOptions = {}) | |||
| 1331 | async function onViewTagsListClick() { | 1339 | async function onViewTagsListClick() { |
| 1332 | const html = $(document.createElement('div')); | 1340 | const html = $(document.createElement('div')); |
| 1333 | html.attr('id', 'tag_view_list'); | 1341 | html.attr('id', 'tag_view_list'); |
| 1334 | html.append(await renderTemplateAsync('tagManagement', { bogus_folders: power_user.bogus_folders, auto_sort_tags: power_user.auto_sort_tags })); | 1342 | html.append(await renderTemplateAsync('tagManagement', { bogus_folders: power_user.bogus_folders })); |
| 1335 | 1343 | ||
| 1336 | const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>'); | 1344 | const tagContainer = $('<div class="tag_view_list_tags ui-sortable"></div>'); |
| 1337 | html.append(tagContainer); | 1345 | html.append(tagContainer); |
| 1338 | 1346 | ||
| 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 | |||
| 1339 | printViewTagList(tagContainer); | 1356 | printViewTagList(tagContainer); |
| 1340 | makeTagListDraggable(tagContainer); | 1357 | makeTagListDraggable(tagContainer); |
| 1341 | 1358 | ||
| 1342 | await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, wide: true, large: true }); | 1359 | await callGenericPopup(html, POPUP_TYPE.TEXT, null, { allowVerticalScrolling: true, wide: true, large: true }); |
| 1343 | } | 1360 | } |
| 1344 | 1361 | ||
| 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 | |||
| 1372 | function makeTagListDraggable(tagContainer) { | 1362 | function makeTagListDraggable(tagContainer) { |
| 1373 | const onTagsSort = () => { | 1363 | const onTagsSort = () => { |
| 1374 | tagContainer.find('.tag_view_item').each(function (i, tagElement) { | 1364 | tagContainer.find('.tag_view_item').each(function (i, tagElement) { |
| @@ -1380,10 +1370,10 @@ function makeTagListDraggable(tagContainer) { | |||
| 1380 | }); | 1370 | }); |
| 1381 | 1371 | ||
| 1382 | // If tags were dragged manually, we have to disable auto sorting | 1372 | // If tags were dragged manually, we have to disable auto sorting |
| 1383 | if (power_user.auto_sort_tags) { | 1373 | if (power_user.tag_sort_mode !== tag_sort_mode.MANUAL) { |
| 1384 | power_user.auto_sort_tags = false; | 1374 | power_user.tag_sort_mode = tag_sort_mode.MANUAL; |
| 1385 | $('#tag_view_list input[name="auto_sort_tags"]').prop('checked', false); | 1375 | $('#tag_sort_mode_select').val(tag_sort_mode.MANUAL); |
| 1386 | toastr.info('Automatic sorting of tags deactivated.'); | 1376 | toastr.info('Switched to Manual sorting mode.'); |
| 1387 | } | 1377 | } |
| 1388 | 1378 | ||
| 1389 | // 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. | 1379 | // 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) { | |||
| 1417 | * @returns {number} The compare result | 1407 | * @returns {number} The compare result |
| 1418 | */ | 1408 | */ |
| 1419 | function compareTagsForSort(a, b) { | 1409 | function compareTagsForSort(a, b) { |
| 1410 | // default sort: alphabetical, case insensitive | ||
| 1420 | const defaultSort = a.name.toLowerCase().localeCompare(b.name.toLowerCase()); | 1411 | 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) { | ||
| 1422 | return defaultSort; | 1420 | return defaultSort; |
| 1423 | } | 1421 | } |
| 1424 | 1422 | ||
| 1423 | // manual sort | ||
| 1425 | if (a.sort_order !== undefined && b.sort_order !== undefined) { | 1424 | if (a.sort_order !== undefined && b.sort_order !== undefined) { |
| 1426 | return a.sort_order - b.sort_order; | 1425 | return a.sort_order - b.sort_order; |
| 1427 | } else if (a.sort_order !== undefined) { | 1426 | } else if (a.sort_order !== undefined) { |
| @@ -1620,8 +1619,13 @@ function onTagCreateClick() { | |||
| 1620 | toastr.success('Tag created', 'Create Tag'); | 1619 | toastr.success('Tag created', 'Create Tag'); |
| 1621 | } | 1620 | } |
| 1622 | 1621 | ||
| 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) { | ||
| 1625 | const template = VIEW_TAG_TEMPLATE.clone(); | 1629 | const template = VIEW_TAG_TEMPLATE.clone(); |
| 1626 | template.attr('id', tag.id); | 1630 | template.attr('id', tag.id); |
| 1627 | template.find('.tag_view_counter_value').text(count); | 1631 | template.find('.tag_view_counter_value').text(count); |
| @@ -1869,12 +1873,21 @@ function copyTags(data) { | |||
| 1869 | tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap])); | 1873 | tag_map[data.newAvatar] = Array.from(new Set([...prevTagMap, ...newTagMap])); |
| 1870 | } | 1874 | } |
| 1871 | 1875 | ||
| 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 | */ | ||
| 1872 | function printViewTagList(tagContainer, empty = true) { | 1881 | function printViewTagList(tagContainer, empty = true) { |
| 1873 | if (empty) tagContainer.empty(); | 1882 | if (empty) tagContainer.empty(); |
| 1874 | const everything = Object.values(tag_map).flat(); | 1883 | 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); | ||
| 1876 | for (const tag of sortedTags) { | 1889 | for (const tag of sortedTags) { |
| 1877 | appendViewTagToList(tagContainer, tag, everything); | 1890 | appendViewTagToList(tagContainer, tag, tag.count); |
| 1878 | } | 1891 | } |
| 1879 | } | 1892 | } |
| 1880 | 1893 | ||
| @@ -2296,14 +2309,9 @@ export function initTags() { | |||
| 2296 | eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags); | 2309 | eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags); |
| 2297 | eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect()); | 2310 | eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect()); |
| 2298 | 2311 | ||
| 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 | }); | ||
| 2304 | $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => { | 2312 | $(document).on('focusout', '#tag_view_list .tag_view_name', (evt) => { |
| 2305 | // Reorder/reprint tags, but only if the name actually has changed, and only if we auto sort tags | 2313 | // Reorder/reprint tags, but only if the name actually has changed |
| 2306 | if (!power_user.auto_sort_tags || !$(evt.target).is('[dirty]')) return; | 2314 | if (!$(evt.target).is('[dirty]')) return; |
| 2307 | 2315 | ||
| 2308 | // Remember the order, so we can flash highlight if it changed after reprinting | 2316 | // Remember the order, so we can flash highlight if it changed after reprinting |
| 2309 | const tagId = ($(evt.target).closest('.tag_view_item')).attr('id'); | 2317 | const tagId = ($(evt.target).closest('.tag_view_item')).attr('id'); |
| @@ -2324,14 +2332,6 @@ export function initTags() { | |||
| 2324 | } | 2332 | } |
| 2325 | }); | 2333 | }); |
| 2326 | 2334 | ||
| 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 | |||
| 2335 | registerTagsSlashCommands(); | 2335 | registerTagsSlashCommands(); |
| 2336 | restoreSavedTagFilters(); | 2336 | restoreSavedTagFilters(); |
| 2337 | } | 2337 | } |
| @@ -21,17 +21,16 @@ | |||
| 21 | </div> | 21 | </div> |
| 22 | </div> | 22 | </div> |
| 23 | <div class="justifyLeft m-b-1"> | 23 | <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> | ||
| 24 | <small> | 32 | <small> |
| 25 | <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> | 33 | <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> |
| 26 | {{#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}} | 34 | {{#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. If disabled, new tags will be appended at the end. If a tag is manually reordered by dragging, automatic sorting will be disabled."> | ||
| 34 | </div> | ||
| 35 | </label> | ||
| 36 | </small> | 35 | </small> |
| 37 | </div> | 36 | </div> |