Merge branch 'staging' into timed-wi

This commit is contained in:
Cohee 2024-06-22 14:39:38 +03:00
commit 6c9f3a868d
11 changed files with 232 additions and 122 deletions

View File

@ -3923,13 +3923,22 @@
<h4 data-i18n="Character Handling">
Character Handling
</h4>
<div title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<div class="flex-container alignitemscenter" title="If set in the advanced character definitions, this field will be displayed in the characters list." data-i18n="[title]If set in the advanced character definitions, this field will be displayed in the characters list.">
<label for="aux_field"><small data-i18n="Char List Subheader">Char List Subheader</small></label>
<select id="aux_field">
<select id="aux_field" class="widthNatural flex1 margin0">
<option data-i18n="Character Version" value="character_version">Character Version</option>
<option data-i18n="Created by" value="creator">Created by</option>
</select>
</div>
<div data-newbie-hidden class="flex-container alignitemscenter" title="Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog." data-i18n="[title]Defines on importing cards which action should be chosen for importing its listed tags. 'Ask' will always display the dialog.">
<label for="tag_import_setting"><small data-i18n="Import Card Tags">Import Card Tags</small></label>
<select id="tag_import_setting" class="widthNatural flex1 margin0">
<option data-i18n="Ask" value="1">Ask</option>
<option data-i18n="None" value="2">None</option>
<option data-i18n="All" value="3">All</option>
<option data-i18n="Existing" value="4">Existing</option>
</select>
</div>
<label data-newbie-hidden class="checkbox_label" for="fuzzy_search_checkbox" title="Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring." data-i18n="[title]Use fuzzy matching, and search characters in the list by all data fields, not just by a name substring">
<input id="fuzzy_search_checkbox" type="checkbox" />
<small data-i18n="Advanced Character Search">Advanced Character Search</small>
@ -3950,10 +3959,6 @@
<input id="show_card_avatar_urls" type="checkbox" />
<small data-i18n="Show avatar filenames">Show avatar filenames</small>
</label>
<label data-newbie-hidden class="checkbox_label" for="import_card_tags" title="Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored." data-i18n="[title]Prompt to import embedded card tags on character import. Otherwise embedded tags are ignored">
<input id="import_card_tags" type="checkbox" />
<small data-i18n="Import Card Tags">Import Card Tags</small>
</label>
<label data-newbie-hidden class="checkbox_label" for="spoiler_free_mode" title="Hide character definitions from the editor panel behind a spoiler button." data-i18n="[title]Hide character definitions from the editor panel behind a spoiler button">
<input id="spoiler_free_mode" type="checkbox" />
<small data-i18n="Spoiler Free Mode">Spoiler Free Mode</small>
@ -5493,7 +5498,7 @@
</div>
</div>
<div id="character_template" class="template_element">
<div class="character_select flex-container wide100p alignitemsflexstart" chid="" id="">
<div class="character_select entity_block flex-container wide100p alignitemsflexstart" chid="" id="">
<div class="avatar" title="">
<img src="">
</div>
@ -5794,7 +5799,7 @@
</div>
</div>
<div id="group_list_template" class="template_element">
<div class="group_select flex-container wide100p alignitemsflexstart">
<div class="group_select entity_block flex-container wide100p alignitemsflexstart">
<div class="avatar">
<img src="">
</div>
@ -5812,7 +5817,7 @@
</div>
</div>
<div id="bogus_folder_template" class="template_element">
<div class="bogus_folder_select flex-container wide100p alignitemsflexstart">
<div class="bogus_folder_select entity_block flex-container wide100p alignitemsflexstart">
<div class="avatar flex alignitemscenter textAlignCenter">
<i class="bogus_folder_icon fa-solid fa-xl"></i>
</div>

View File

@ -175,11 +175,12 @@ import {
createTagMapFromList,
renameTagKey,
importTags,
tag_filter_types,
tag_filter_type,
compareTagsForSort,
initTags,
applyTagsOnCharacterSelect,
applyTagsOnGroupSelect,
tag_import_setting,
} from './scripts/tags.js';
import {
SECRET_KEYS,
@ -1346,8 +1347,8 @@ export async function printCharacters(fullRefresh = false) {
verifyCharactersSearchSortRule();
// We are actually always reprinting filters, as it "doesn't hurt", and this way they are always up to date
printTagFilters(tag_filter_types.character);
printTagFilters(tag_filter_types.group_member);
printTagFilters(tag_filter_type.character);
printTagFilters(tag_filter_type.group_member);
// We are also always reprinting the lists on character/group edit window, as these ones doesn't get updated otherwise
applyTagsOnCharacterSelect();
@ -1368,7 +1369,7 @@ export async function printCharacters(fullRefresh = false) {
nextText: '>',
formatNavigator: PAGINATION_TEMPLATE,
showNavigator: true,
callback: function (data) {
callback: function (/** @type {Entity[]} */ data) {
$(listId).empty();
if (power_user.bogus_folders && isBogusFolderOpen()) {
$(listId).append(getBackBlock());
@ -1388,7 +1389,7 @@ export async function printCharacters(fullRefresh = false) {
displayCount++;
break;
case 'tag':
$(listId).append(getTagBlock(i.item, i.entities, i.hidden));
$(listId).append(getTagBlock(i.item, i.entities, i.hidden, i.isUseless));
break;
}
}
@ -1442,8 +1443,9 @@ function verifyCharactersSearchSortRule() {
* @property {Character|Group|import('./scripts/tags.js').Tag|*} item - The item
* @property {string|number} id - The id
* @property {'character'|'group'|'tag'} type - The type of this entity (character, group, tag)
* @property {Entity[]} [entities] - An optional list of entities relevant for this item
* @property {number} [hidden] - An optional number representing how many hidden entities this entity contains
* @property {Entity[]?} [entities=null] - An optional list of entities relevant for this item
* @property {number?} [hidden=null] - An optional number representing how many hidden entities this entity contains
* @property {boolean?} [isUseless=null] - Specifies if the entity is useless (not relevant, but should still be displayed for consistency) and should be displayed greyed out
*/
/**
@ -1538,6 +1540,15 @@ export function getEntitiesList({ doFilter = false, doSort = true } = {}) {
}
}
// Final step, updating some properties after the last filter run
const nonTagEntitiesCount = entities.filter(entity => entity.type !== 'tag').length;
for (const entity of entities) {
if (entity.type === 'tag') {
if (entity.entities?.length == nonTagEntitiesCount) entity.isUseless = true;
}
}
// Sort before returning if requested
if (doSort) {
sortEntitiesList(entities);
}
@ -8464,7 +8475,7 @@ async function importCharacter(file, preserveFileName = false) {
await getCharacters();
select_rm_info('char_import', data.file_name, oldSelectedChar);
if (power_user.import_card_tags) {
if (power_user.tag_import_setting !== tag_import_setting.NONE) {
let currentContext = getContext();
let avatarFileName = `${data.file_name}.png`;
let importedCharacter = currentContext.characters.find(character => character.avatar === avatarFileName);
@ -10599,7 +10610,7 @@ jQuery(async function () {
}
} break;
case 'import_tags': {
await importTags(characters[this_chid]);
await importTags(characters[this_chid], { forceShow: true });
} break;
/*case 'delete_button':
popup_type = "del_ch";

View File

@ -1,4 +1,4 @@
import { getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
import { ensureImageFormatSupported, getBase64Async, isTrueBoolean, saveBase64AsFile } from '../../utils.js';
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules, renderExtensionTemplateAsync } from '../../extensions.js';
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParamsExtended } from '../../../script.js';
import { getMessageTimeStamp } from '../../RossAscends-mods.js';
@ -272,7 +272,7 @@ async function getCaptionForFile(file, prompt, quiet) {
try {
setSpinnerIcon();
const context = getContext();
const fileData = await getBase64Async(file);
const fileData = await getBase64Async(await ensureImageFormatSupported(file));
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
const base64Data = fileData.split(',')[1];
const { caption } = await doCaptionRequest(base64Data, fileData, prompt);

View File

@ -28,6 +28,8 @@ export const POPUP_RESULT = {
* @property {boolean?} [allowVerticalScrolling] - Whether to allow vertical scrolling in the popup
* @property {POPUP_RESULT|number?} [defaultResult] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
* @property {CustomPopupButton[]|string[]?} [customButtons] - Custom buttons to add to the popup. If only strings are provided, the buttons will be added with default options, and their result will be in order from `2` onward.
* @property {(popup: Popup) => boolean?} [onClosing] - Handler called before the popup closes, return `false` to cancel the close
* @property {(popup: Popup) => void?} [onClose] - Handler called after the popup closes, but before the DOM is cleaned up
*/
/**
@ -78,6 +80,9 @@ export class Popup {
/** @type {POPUP_RESULT|number?} */ defaultResult;
/** @type {CustomPopupButton[]|string[]?} */ customButtons;
/** @type {(popup: Popup) => boolean?} */ onClosing;
/** @type {(popup: Popup) => void?} */ onClose;
/** @type {POPUP_RESULT|number} */ result;
/** @type {any} */ value;
@ -94,13 +99,17 @@ export class Popup {
* @param {string} [inputValue=''] - The initial value of the input field
* @param {PopupOptions} [options={}] - Additional options for the popup
*/
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null } = {}) {
constructor(content, type, inputValue = '', { okButton = null, cancelButton = null, rows = 1, wide = false, wider = false, large = false, allowHorizontalScrolling = false, allowVerticalScrolling = false, defaultResult = POPUP_RESULT.AFFIRMATIVE, customButtons = null, onClosing = null, onClose = null } = {}) {
Popup.util.popups.push(this);
// Make this popup uniquely identifiable
this.id = uuidv4();
this.type = type;
// Utilize event handlers being passed in
this.onClosing = onClosing;
this.onClose = onClose;
/**@type {HTMLTemplateElement}*/
const template = document.querySelector('#popup_template');
// @ts-ignore
@ -317,6 +326,12 @@ export class Popup {
this.value = value;
this.result = result;
if (this.onClosing) {
const shouldClose = this.onClosing(this);
if (!shouldClose) return;
}
Popup.util.lastResult = { value, result };
this.hide();
}
@ -337,6 +352,11 @@ export class Popup {
// Call the close on the dialog
this.dlg.close();
// Run a possible custom handler right before DOM removal
if (this.onClose) {
this.onClose(this);
}
// Remove it from the dom
this.dlg.remove();

View File

@ -35,7 +35,7 @@ import {
selectInstructPreset,
} from './instruct-mode.js';
import { getTagsList, tag_map, tags } from './tags.js';
import { getTagsList, tag_import_setting, tag_map, tags } from './tags.js';
import { tokenizers } from './tokenizers.js';
import { BIAS_CACHE } from './logit-bias.js';
import { renderTemplateAsync } from './templates.js';
@ -198,6 +198,7 @@ let power_user = {
trim_spaces: true,
relaxed_api_urls: false,
world_import_dialog: true,
tag_import_setting: tag_import_setting.ASK,
disable_group_trimming: false,
single_line: false,
@ -1562,6 +1563,12 @@ function loadPowerUserSettings(settings, data) {
power_user.tokenizer = tokenizers.GPT2;
}
// Clean up old/legacy settings
if (power_user.import_card_tags !== undefined) {
power_user.tag_import_setting = power_user.import_card_tags ? tag_import_setting.ASK : tag_import_setting.NONE;
delete power_user.import_card_tags;
}
$('#single_line').prop('checked', power_user.single_line);
$('#relaxed_api_urls').prop('checked', power_user.relaxed_api_urls);
$('#world_import_dialog').prop('checked', power_user.world_import_dialog);
@ -1590,7 +1597,6 @@ function loadPowerUserSettings(settings, data) {
$('#zoomed_avatar_magnification').prop('checked', power_user.zoomed_avatar_magnification);
$(`#tokenizer option[value="${power_user.tokenizer}"]`).attr('selected', true);
$(`#send_on_enter option[value=${power_user.send_on_enter}]`).attr('selected', true);
$('#import_card_tags').prop('checked', power_user.import_card_tags);
$('#confirm_message_delete').prop('checked', power_user.confirm_message_delete !== undefined ? !!power_user.confirm_message_delete : true);
$('#spoiler_free_mode').prop('checked', power_user.spoiler_free_mode);
$('#collapse-newlines-checkbox').prop('checked', power_user.collapse_newlines);
@ -1632,6 +1638,7 @@ function loadPowerUserSettings(settings, data) {
$('#chat_width_slider').val(power_user.chat_width);
$('#token_padding').val(power_user.token_padding);
$('#aux_field').val(power_user.aux_field);
$('#tag_import_setting').val(power_user.tag_import_setting);
$('#stscript_autocomplete_autoHide').prop('checked', power_user.stscript.autocomplete.autoHide ?? false).trigger('input');
$('#stscript_matching').val(power_user.stscript.matching ?? 'fuzzy');
@ -3516,11 +3523,6 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#import_card_tags').on('input', function () {
power_user.import_card_tags = !!$(this).prop('checked');
saveSettingsDebounced();
});
$('#confirm_message_delete').on('input', function () {
power_user.confirm_message_delete = !!$(this).prop('checked');
saveSettingsDebounced();
@ -3759,6 +3761,12 @@ $(document).ready(() => {
saveSettingsDebounced();
});
$('#tag_import_setting').on('change', function () {
const value = $(this).find(':selected').val();
power_user.tag_import_setting = Number(value);
saveSettingsDebounced();
});
$('#stscript_autocomplete_autoHide').on('input', function () {
power_user.stscript.autocomplete.autoHide = !!$(this).prop('checked');
saveSettingsDebounced();

View File

@ -2,7 +2,6 @@ import {
characters,
saveSettingsDebounced,
this_chid,
callPopup,
menu_type,
entitiesFilter,
printCharactersDebounced,
@ -21,11 +20,12 @@ import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
import { SlashCommand } from './slash-commands/SlashCommand.js';
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
import { isMobile } from './RossAscends-mods.js';
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
import { debounce_timeout } from './constants.js';
import { INTERACTABLE_CONTROL_CLASS } from './keyboard.js';
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
import { SlashCommandExecutor } from './slash-commands/SlashCommandExecutor.js';
import { renderTemplateAsync } from './templates.js';
export {
TAG_FOLDER_TYPES,
@ -62,11 +62,20 @@ function getFilterHelper(listSelector) {
return $(listSelector).is(GROUP_FILTER_SELECTOR) ? groupCandidatesFilter : entitiesFilter;
}
export const tag_filter_types = {
/** @enum {number} */
export const tag_filter_type = {
character: 0,
group_member: 1,
};
/** @enum {number} */
export const tag_import_setting = {
ASK: 1,
NONE: 2,
ALL: 3,
ONLY_EXISTING: 4,
};
/**
* @type {{ FAV: Tag, GROUP: Tag, FOLDER: Tag, VIEW: Tag, HINT: Tag, UNFILTER: Tag }}
* A collection of global actional tags for the filter panel
@ -242,16 +251,23 @@ function isBogusFolder(tag) {
}
/**
* Indicates whether a user is currently in a bogus folder.
* Retrieves all currently open bogus folders
*
* @return {Tag[]} An array of open bogus folders
*/
function getOpenBogusFolders() {
return entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected
.map(tagId => tags.find(x => x.id === tagId))
.filter(isBogusFolder) ?? [];
}
/**
* Indicates whether a user is currently in a bogus folder
*
* @returns {boolean} If currently viewing a folder
*/
function isBogusFolderOpen() {
const anyIsFolder = entitiesFilter.getFilterData(FILTER_TYPES.TAG)?.selected
.map(tagId => tags.find(x => x.id === tagId))
.some(isBogusFolder);
return !!anyIsFolder;
return getOpenBogusFolders().length > 0;
}
/**
@ -283,11 +299,12 @@ function chooseBogusFolder(source, tagId, remove = false) {
* Builds the tag block for the specified item.
*
* @param {Tag} tag The tag item
* @param {*} entities The list ob sub items for this tag
* @param {*} hidden A count of how many sub items are hidden
* @param {any[]} entities The list ob sub items for this tag
* @param {number} hidden A count of how many sub items are hidden
* @param {boolean} isUseless Whether the tag is useless (should be displayed greyed out)
* @returns The html for the tag block
*/
function getTagBlock(tag, entities, hidden = 0) {
function getTagBlock(tag, entities, hidden = 0, isUseless = false) {
let count = entities.length;
const tagFolder = TAG_FOLDER_TYPES[tag.folder_type];
@ -300,6 +317,7 @@ function getTagBlock(tag, entities, hidden = 0) {
template.find('.bogus_folder_hidden_counter').text(hidden > 0 ? `${hidden} hidden` : '');
template.find('.bogus_folder_counter').text(`${count} ${count != 1 ? 'characters' : 'character'}`);
template.find('.bogus_folder_icon').addClass(tagFolder.fa_icon);
if (isUseless) template.addClass('useless');
// Fill inline character images
buildAvatarList(template.find('.bogus_folder_avatars_block'), entities);
@ -660,15 +678,6 @@ function getExistingTags(newTags) {
return existingTags;
}
const tagImportSettings = {
ALWAYS_IMPORT_ALL: 1,
ONLY_IMPORT_EXISTING: 2,
IMPORT_NONE: 3,
ASK: 4,
};
let globalTagImportSetting = tagImportSettings.ASK; // Default setting
const IMPORT_EXLCUDED_TAGS = ['ROOT', 'TAVERN'];
const ANTI_TROLL_MAX_TAGS = 15;
@ -676,11 +685,13 @@ const ANTI_TROLL_MAX_TAGS = 15;
* Imports tags for a given character
*
* @param {Character} character - The character
* @param {object} [options] - Options
* @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog
* @returns {Promise<boolean>} Boolean indicating whether any tag was imported
*/
async function importTags(character) {
async function importTags(character, { forceShow = false } = {}) {
// Gather the tags to import based on the selected setting
const tagNamesToImport = await handleTagImport(character);
const tagNamesToImport = await handleTagImport(character, { forceShow });
if (!tagNamesToImport?.length) {
toastr.info('No tags imported', 'Importing Tags');
return;
@ -698,9 +709,11 @@ async function importTags(character) {
* Handles the import of tags for a given character and returns the resulting list of tags to add
*
* @param {Character} character - The character
* @param {object} [options] - Options
* @param {boolean} [options.forceShow=false] - Whether to force showing the import dialog
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function handleTagImport(character) {
async function handleTagImport(character, { forceShow = false } = {}) {
/** @type {string[]} */
const importTags = character.tags.map(t => t.trim()).filter(t => t)
.filter(t => !IMPORT_EXLCUDED_TAGS.includes(t))
@ -708,17 +721,22 @@ async function handleTagImport(character) {
const existingTags = getExistingTags(importTags);
const newTags = importTags.filter(t => !existingTags.some(existingTag => existingTag.name.toLowerCase() === t.toLowerCase()))
.map(newTag);
const folderTags = getOpenBogusFolders();
switch (globalTagImportSetting) {
case tagImportSettings.ALWAYS_IMPORT_ALL:
return existingTags.concat(newTags).map(t => t.name);
case tagImportSettings.ONLY_IMPORT_EXISTING:
return existingTags.map(t => t.name);
case tagImportSettings.ASK:
return await showTagImportPopup(character, existingTags, newTags);
case tagImportSettings.IMPORT_NONE:
default:
// Choose the setting for this dialog. If from settings, verify the setting really exists, otherwise take "ASK".
const setting = forceShow ? tag_import_setting.ASK
: Object.values(tag_import_setting).find(setting => setting === power_user.tag_import_setting) ?? tag_import_setting.ASK;
switch (setting) {
case tag_import_setting.ALL:
return [...existingTags, ...newTags, ...folderTags].map(t => t.name);
case tag_import_setting.ONLY_EXISTING:
return [...existingTags, ...folderTags].map(t => t.name);
case tag_import_setting.ASK:
return await showTagImportPopup(character, existingTags, newTags, folderTags);
case tag_import_setting.NONE:
return [];
default: throw new Error(`Invalid tag import setting: ${setting}`);
}
}
@ -728,63 +746,55 @@ async function handleTagImport(character) {
* @param {Character} character - The character
* @param {Tag[]} existingTags - List of existing tags
* @param {Tag[]} newTags - List of new tags
* @param {Tag[]} folderTags - List of tags in the current folder
* @returns {Promise<string[]>} Array of strings representing the tags to import
*/
async function showTagImportPopup(character, existingTags, newTags) {
async function showTagImportPopup(character, existingTags, newTags, folderTags) {
/** @type {{[key: string]: import('./popup.js').CustomPopupButton}} */
const importButtons = {
EXISTING: { result: 2, text: 'Import Existing' },
NONE: { result: 2, text: 'Import None', },
ALL: { result: 3, text: 'Import All' },
NONE: { result: 4, text: 'Import None' },
EXISTING: { result: 4, text: 'Import Existing' },
};
const buttonSettingsMap = {
[POPUP_RESULT.AFFIRMATIVE]: tag_import_setting.ASK,
[importButtons.NONE.result]: tag_import_setting.NONE,
[importButtons.ALL.result]: tag_import_setting.ALL,
[importButtons.EXISTING.result]: tag_import_setting.ONLY_EXISTING,
};
const customButtonsCaptions = Object.values(importButtons).map(button => `&quot;${button.text}&quot;`);
const customButtonsString = customButtonsCaptions.slice(0, -1).join(', ') + ' or ' + customButtonsCaptions.slice(-1);
const popupContent = $(`
<h3>Import Tags For ${character.name}</h3>
<div class="import_avatar_placeholder"></div>
<div class="import_tags_content justifyLeft">
<small>
Click remove on any tag to remove it from this import.<br />
Select one of the import options to finish importing the tags.
</small>
<h4 class="m-t-1">Existing Tags</h4>
<div id="import_existing_tags_list" class="tags"></div>
<h4 class="m-t-1">New Tags</h4>
<div id="import_new_tags_list" class="tags"></div>
<small>
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
<span data-i18n="Remember my choice">
Remember my choice
<div class="fa-solid fa-circle-info opacity50p" data-i18n="[title]Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;."
title="Remember the chosen import option\nIf ${customButtonsString} is selected, this dialog will not show up anymore.\nTo change this, go to the settings and modify &quot;Tag Import Option&quot;.\n\nIf the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;.">
</div>
</span>
</label>
</small>
</div>`);
const popupContent = $(await renderTemplateAsync('charTagImport', { charName: character.name }));
// Print tags after popup is shown, so that events can be added
printTagList(popupContent.find('#import_existing_tags_list'), { tags: existingTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(existingTags, tag) } });
printTagList(popupContent.find('#import_new_tags_list'), { tags: newTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(newTags, tag) } });
printTagList(popupContent.find('#import_folder_tags_list'), { tags: folderTags, tagOptions: { removable: true, removeAction: tag => removeFromArray(folderTags, tag) } });
const result = await callGenericPopup(popupContent, POPUP_TYPE.TEXT, null, { wider: true, okButton: 'Import', cancelButton: true, customButtons: Object.values(importButtons) });
if (folderTags.length === 0) popupContent.find('#folder_tags_block').hide();
function onCloseRemember(/** @type {Popup} */ popup) {
const rememberCheckbox = document.getElementById('import_remember_option');
if (rememberCheckbox instanceof HTMLInputElement && rememberCheckbox.checked) {
const setting = buttonSettingsMap[popup.result];
if (!setting) return;
power_user.tag_import_setting = setting;
$('#tag_import_setting').val(power_user.tag_import_setting);
saveSettingsDebounced();
console.log('Remembered tag import setting:', Object.entries(tag_import_setting).find(x => x[1] === setting)[0], setting);
}
}
const result = await callGenericPopup(popupContent, POPUP_TYPE.TEXT, null, { wider: true, okButton: 'Import', cancelButton: true, customButtons: Object.values(importButtons), onClose: onCloseRemember });
if (!result) {
return [];
}
switch (result) {
case 1:
case true:
case importButtons.ALL.result: // Default 'Import' option where it imports all selected
return existingTags.concat(newTags).map(t => t.name);
case POPUP_RESULT.AFFIRMATIVE: // Default 'Import' option where it imports all selected
case importButtons.ALL.result:
return [...existingTags, ...newTags, ...folderTags].map(t => t.name);
case importButtons.EXISTING.result:
return existingTags.map(t => t.name);
return [...existingTags, ...folderTags].map(t => t.name);
case importButtons.NONE.result:
default:
return [];
@ -1113,8 +1123,8 @@ function runTagFilters(listElement) {
filterHelper.setFilterData(FILTER_TYPES.TAG, { excluded: excludedTagIds, selected: tagIds });
}
function printTagFilters(type = tag_filter_types.character) {
const FILTER_SELECTOR = type === tag_filter_types.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
function printTagFilters(type = tag_filter_type.character) {
const FILTER_SELECTOR = type === tag_filter_type.character ? CHARACTER_FILTER_SELECTOR : GROUP_FILTER_SELECTOR;
$(FILTER_SELECTOR).empty();
// Print all action tags. (Rework 'Folder' button to some kind of onboarding if no folders are enabled yet)
@ -1133,9 +1143,7 @@ function printTagFilters(type = tag_filter_types.character) {
const bogusDrilldown = $(FILTER_SELECTOR).siblings('.rm_tag_bogus_drilldown');
bogusDrilldown.empty();
if (power_user.bogus_folders && bogusDrilldown.length > 0) {
const filterData = structuredClone(entitiesFilter.getFilterData(FILTER_TYPES.TAG));
const navigatedTags = filterData.selected.map(x => tags.find(t => t.id == x)).filter(x => isBogusFolder(x));
const navigatedTags = getOpenBogusFolders();
printTagList(bogusDrilldown, { tags: navigatedTags, tagOptions: { removable: true } });
}

View File

@ -0,0 +1,35 @@
<h3>Import Tags For {{charName}}</h3>
<div class="import_avatar_placeholder"></div>
<div class="import_tags_content justifyLeft">
<small data-i18n="Click remove on any tag to remove it from this import.&lt;br /&gt;Select one of the import options to finish importing the tags.">
Click remove on any tag to remove it from this import.<br />
Select one of the import options to finish importing the tags.
</small>
<h4 class="m-t-1" data-i18n="Existing Tags">Existing Tags</h4>
<div id="import_existing_tags_list" class="tags" style="min-height: 20px;"></div>
<h4 class="m-t-1" data-i18n="New Tags">New Tags</h4>
<div id="import_new_tags_list" class="tags" style="min-height: 20px;"></div>
<div id="folder_tags_block" class="m-t-1">
<h4 data-i18n="Folder Tags">Folder Tags</h4>
<small data-i18n="The following tags will be auto-imported based on the currently selected folders">
The following tags will be auto-imported based on the currently selected folders
</small>
<div id="import_folder_tags_list" class="tags" style="margin-top: 5px;"></div>
</div>
<small>
<label class="checkbox flex-container alignitemscenter flexNoGap m-t-3" for="import_remember_option">
<input type="checkbox" id="import_remember_option" name="import_remember_option" />
<span data-i18n="Remember my choice">
Remember my choice
<div class="fa-solid fa-circle-info opacity50p"
data-i18n="[title]Remember the chosen import option&#010;If anything besides 'Cancel' is selected, this dialog will not show up anymore.&#010;To change this, go to the settings and modify &quot;Tag Import Option&quot;.&#010;&#010;If the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;."
title="Remember the chosen import option&#010;If anything besides 'Cancel' is selected, this dialog will not show up anymore.&#010;To change this, go to the settings and modify &quot;Tag Import Option&quot;.&#010;&#010;If the &quot;Import&quot; option is chosen, the global setting will stay on &quot;Ask&quot;.">
</div>
</span>
</label>
</small>
</div>

View File

@ -39,6 +39,8 @@ const OPENROUTER_PROVIDERS = [
'Novita',
'Lynn',
'Lynn 2',
'DeepSeek',
'Infermatic',
];
export async function loadOllamaModels(data) {

View File

@ -367,7 +367,12 @@ input[type='checkbox']:focus-visible {
}
.img_enlarged_container {
display: flex;
flex-direction: column;
justify-content: flex-end;
padding: 10px;
height: 100%;
width: 100%;
}
.img_enlarged_container pre code,
@ -2363,6 +2368,10 @@ input[type="file"] {
padding: 1px;
}
#rm_print_characters_block .entity_block.useless {
opacity: 0.25;
}
#rm_ch_create_block {
display: none;
overflow-y: auto;
@ -4479,7 +4488,7 @@ a {
max-height: 100%;
border-radius: 2px;
border: 1px solid transparent;
outline: 1px solid var(--SmartThemeBorderColor);
object-fit: contain;
}
.cropper-container {

View File

@ -6,6 +6,7 @@ const fs = require('fs');
const { jsonParser, urlencodedParser } = require('../express-common');
const { getConfigValue, mergeObjectWithYaml, excludeKeysByYaml, trimV1 } = require('../util');
const { setAdditionalHeaders } = require('../additional-headers');
const { OPENROUTER_HEADERS } = require('../constants');
const router = express.Router();
@ -80,7 +81,7 @@ router.post('/caption-image', jsonParser, async (request, response) => {
if (request.body.api === 'openrouter') {
apiUrl = 'https://openrouter.ai/api/v1/chat/completions';
headers['HTTP-Referer'] = request.headers.referer;
Object.assign(headers, OPENROUTER_HEADERS);
}
if (request.body.api === 'openai') {

View File

@ -147,25 +147,36 @@ router.post('/searxng', jsonParser, async (request, response) => {
console.log('SearXNG query', baseUrl, query);
const url = new URL(baseUrl);
const params = new URLSearchParams();
params.append('q', query);
params.append('format', 'html');
url.pathname = '/search';
url.search = params.toString();
const mainPageUrl = new URL(baseUrl);
const mainPageRequest = await fetch(mainPageUrl, { headers: visitHeaders });
const result = await fetch(url, {
method: 'POST',
headers: visitHeaders,
});
if (!result.ok) {
const text = await result.text();
console.log('SearXNG request failed', result.statusText, text);
if (!mainPageRequest.ok) {
console.log('SearXNG request failed', mainPageRequest.statusText);
return response.sendStatus(500);
}
const data = await result.text();
const mainPageText = await mainPageRequest.text();
const clientHref = mainPageText.match(/href="(\/client.+\.css)"/)?.[1];
if (clientHref) {
const clientUrl = new URL(clientHref, baseUrl);
await fetch(clientUrl, { headers: visitHeaders });
}
const searchUrl = new URL('/search', baseUrl);
const searchParams = new URLSearchParams();
searchParams.append('q', query);
searchUrl.search = searchParams.toString();
const searchResult = await fetch(searchUrl, { headers: visitHeaders });
if (!searchResult.ok) {
const text = await searchResult.text();
console.log('SearXNG request failed', searchResult.statusText, text);
return response.sendStatus(500);
}
const data = await searchResult.text();
return response.send(data);
} catch (error) {
console.log('SearXNG request failed', error);