diff --git a/public/index.html b/public/index.html index e928a6442..4b10d3f7c 100644 --- a/public/index.html +++ b/public/index.html @@ -3923,13 +3923,22 @@

Character Handling

-
+
-
+
+ + +
-
-
+
@@ -5794,7 +5799,7 @@
-
+
@@ -5812,7 +5817,7 @@
-
+
diff --git a/public/script.js b/public/script.js index ca2765ef8..29df8a38e 100644 --- a/public/script.js +++ b/public/script.js @@ -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"; diff --git a/public/scripts/extensions/caption/index.js b/public/scripts/extensions/caption/index.js index d981c15a6..d7a167fd4 100644 --- a/public/scripts/extensions/caption/index.js +++ b/public/scripts/extensions/caption/index.js @@ -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); diff --git a/public/scripts/popup.js b/public/scripts/popup.js index be460feb6..6e388839c 100644 --- a/public/scripts/popup.js +++ b/public/scripts/popup.js @@ -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(); diff --git a/public/scripts/power-user.js b/public/scripts/power-user.js index 545eb1454..bc43a8ca0 100644 --- a/public/scripts/power-user.js +++ b/public/scripts/power-user.js @@ -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(); diff --git a/public/scripts/tags.js b/public/scripts/tags.js index 519fc47eb..24a57a9c0 100644 --- a/public/scripts/tags.js +++ b/public/scripts/tags.js @@ -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 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} 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} 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 => `"${button.text}"`); - const customButtonsString = customButtonsCaptions.slice(0, -1).join(', ') + ' or ' + customButtonsCaptions.slice(-1); - - const popupContent = $(` -

Import Tags For ${character.name}

-
-
- - Click remove on any tag to remove it from this import.
- Select one of the import options to finish importing the tags. -
- -

Existing Tags

-
- -

New Tags

-
- - - - -
`); + 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 } }); } diff --git a/public/scripts/templates/charTagImport.html b/public/scripts/templates/charTagImport.html new file mode 100644 index 000000000..e898dd082 --- /dev/null +++ b/public/scripts/templates/charTagImport.html @@ -0,0 +1,35 @@ +

Import Tags For {{charName}}

+
+
+ + Click remove on any tag to remove it from this import.
+ Select one of the import options to finish importing the tags. +
+ +

Existing Tags

+
+ +

New Tags

+
+ +
+

Folder Tags

+ + The following tags will be auto-imported based on the currently selected folders + +
+
+ + + + +
diff --git a/public/scripts/textgen-models.js b/public/scripts/textgen-models.js index d8f36cf45..5f663c816 100644 --- a/public/scripts/textgen-models.js +++ b/public/scripts/textgen-models.js @@ -39,6 +39,8 @@ const OPENROUTER_PROVIDERS = [ 'Novita', 'Lynn', 'Lynn 2', + 'DeepSeek', + 'Infermatic', ]; export async function loadOllamaModels(data) { diff --git a/public/style.css b/public/style.css index 75a77295d..96a1ef69e 100644 --- a/public/style.css +++ b/public/style.css @@ -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 { diff --git a/src/endpoints/openai.js b/src/endpoints/openai.js index 9488d03e6..75de75b7b 100644 --- a/src/endpoints/openai.js +++ b/src/endpoints/openai.js @@ -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') { diff --git a/src/endpoints/search.js b/src/endpoints/search.js index 7f08d89c8..457124228 100644 --- a/src/endpoints/search.js +++ b/src/endpoints/search.js @@ -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);