Merge branch 'staging' into slash-command-enums
This commit is contained in:
commit
eba0f54477
|
@ -109,7 +109,6 @@ dialog {
|
|||
|
||||
.menu_button.popup-button-ok {
|
||||
background-color: var(--crimson70a);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu_button.popup-button-ok:hover {
|
||||
|
@ -132,3 +131,17 @@ dialog {
|
|||
filter: brightness(1.3) saturate(1.3);
|
||||
}
|
||||
|
||||
.popup .popup-button-close {
|
||||
position: absolute;
|
||||
top: -6px;
|
||||
right: -6px;
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
font-size: 20px;
|
||||
padding: 2px 3px 3px 2px;
|
||||
|
||||
filter: brightness(0.8);
|
||||
|
||||
/* Fix weird animation issue with font-scaling during popup open */
|
||||
backface-visibility: hidden;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
@ -4850,10 +4855,11 @@
|
|||
</div>
|
||||
<textarea class="popup-input text_pole result-control" rows="1" data-result="1"></textarea>
|
||||
<div class="popup-controls">
|
||||
<div class="popup-button-ok menu_button result-control" data-i18n="Delete" data-result="1" tabindex="0">Delete</div>
|
||||
<div class="popup-button-cancel menu_button result-control" data-i18n="Cancel" data-result="0" tabindex="0">Cancel</div>
|
||||
<div class="popup-button-ok menu_button result-control" data-result="1" data-i18n="Delete">Delete</div>
|
||||
<div class="popup-button-cancel menu_button result-control" data-result="0" data-i18n="Cancel">Cancel</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="popup-button-close right_menu_button fa-solid fa-circle-xmark" data-result="0" title="Close popup" data-i18n="[title]Close popup"></div>
|
||||
</dialog>
|
||||
</template>
|
||||
<div id="shadow_popup">
|
||||
|
@ -5465,7 +5471,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>
|
||||
|
@ -5766,7 +5772,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>
|
||||
|
@ -5784,7 +5790,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>
|
||||
|
|
|
@ -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,
|
||||
|
@ -1348,8 +1349,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();
|
||||
|
@ -1370,7 +1371,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());
|
||||
|
@ -1390,7 +1391,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;
|
||||
}
|
||||
}
|
||||
|
@ -1444,8 +1445,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
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -1540,6 +1542,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);
|
||||
}
|
||||
|
@ -2113,7 +2124,7 @@ export function addCopyToCodeBlocks(messageElement) {
|
|||
hljs.highlightElement(codeBlocks.get(i));
|
||||
if (navigator.clipboard !== undefined) {
|
||||
const copyButton = document.createElement('i');
|
||||
copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy');
|
||||
copyButton.classList.add('fa-solid', 'fa-copy', 'code-copy', 'interactable');
|
||||
copyButton.title = 'Copy code';
|
||||
codeBlocks.get(i).appendChild(copyButton);
|
||||
copyButton.addEventListener('pointerup', function (event) {
|
||||
|
@ -8470,7 +8481,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);
|
||||
|
@ -10610,7 +10621,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";
|
||||
|
|
|
@ -35,7 +35,7 @@ import {
|
|||
extractTextFromOffice,
|
||||
} from './utils.js';
|
||||
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
|
||||
import { ScraperManager } from './scrapers.js';
|
||||
import { DragAndDropHandler } from './dragdrop.js';
|
||||
|
||||
|
@ -566,7 +566,7 @@ export function isExternalMediaAllowed() {
|
|||
return !power_user.forbid_external_media;
|
||||
}
|
||||
|
||||
function enlargeMessageImage() {
|
||||
async function enlargeMessageImage() {
|
||||
const mesBlock = $(this).closest('.mes');
|
||||
const mesId = mesBlock.attr('mesid');
|
||||
const message = chat[mesId];
|
||||
|
@ -580,14 +580,28 @@ function enlargeMessageImage() {
|
|||
const img = document.createElement('img');
|
||||
img.classList.add('img_enlarged');
|
||||
img.src = imgSrc;
|
||||
const imgHolder = document.createElement('div');
|
||||
imgHolder.classList.add('img_enlarged_holder');
|
||||
imgHolder.append(img);
|
||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||
imgContainer.prepend(img);
|
||||
imgContainer.prepend(imgHolder);
|
||||
imgContainer.addClass('img_enlarged_container');
|
||||
imgContainer.find('code').addClass('txt').text(title);
|
||||
const titleEmpty = !title || title.trim().length === 0;
|
||||
imgContainer.find('pre').toggle(!titleEmpty);
|
||||
addCopyToCodeBlocks(imgContainer);
|
||||
callGenericPopup(imgContainer, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||
|
||||
const popup = new Popup(imgContainer, POPUP_TYPE.DISPLAY, '', { large: true, transparent: true });
|
||||
|
||||
popup.dlg.style.width = 'unset';
|
||||
popup.dlg.style.height = 'unset';
|
||||
|
||||
img.addEventListener('click', () => {
|
||||
const shouldZoom = !img.classList.contains('zoomed');
|
||||
img.classList.toggle('zoomed', shouldZoom);
|
||||
});
|
||||
|
||||
await popup.show();
|
||||
}
|
||||
|
||||
async function deleteMessageImage() {
|
||||
|
|
|
@ -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';
|
||||
|
@ -274,7 +274,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);
|
||||
|
@ -379,6 +379,12 @@ jQuery(async function () {
|
|||
}
|
||||
function switchMultimodalBlocks() {
|
||||
const isMultimodal = extension_settings.caption.source === 'multimodal';
|
||||
$('#caption_ollama_pull').on('click', (e) => {
|
||||
const presetModel = extension_settings.caption.multimodal_model !== 'ollama_current' ? extension_settings.caption.multimodal_model : '';
|
||||
e.preventDefault();
|
||||
$('#ollama_download_model').trigger('click');
|
||||
$('#dialogue_popup_input').val(presetModel);
|
||||
});
|
||||
$('#caption_multimodal_block').toggle(isMultimodal);
|
||||
$('#caption_prompt_block').toggle(isMultimodal);
|
||||
$('#caption_multimodal_api').val(extension_settings.caption.multimodal_api);
|
||||
|
|
|
@ -58,14 +58,20 @@
|
|||
<option data-type="openrouter" value="google/gemini-flash-1.5">google/gemini-flash-1.5</option>
|
||||
<option data-type="openrouter" value="liuhaotian/llava-yi-34b">liuhaotian/llava-yi-34b</option>
|
||||
<option data-type="ollama" value="ollama_current" data-i18n="currently_selected">[Currently selected]</option>
|
||||
<option data-type="ollama" value="bakllava:latest">bakllava:latest</option>
|
||||
<option data-type="ollama" value="llava:latest">llava:latest</option>
|
||||
<option data-type="ollama" value="bakllava">bakllava</option>
|
||||
<option data-type="ollama" value="llava">llava</option>
|
||||
<option data-type="ollama" value="llava-llama3">llava-llama3</option>
|
||||
<option data-type="ollama" value="llava-phi3">llava-phi3</option>
|
||||
<option data-type="ollama" value="moondream">moondream</option>
|
||||
<option data-type="llamacpp" value="llamacpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="ooba" value="ooba_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="koboldcpp" value="koboldcpp_current" data-i18n="currently_loaded">[Currently loaded]</option>
|
||||
<option data-type="custom" value="custom_current" data-i18n="currently_selected">[Currently selected]</option>
|
||||
</select>
|
||||
</div>
|
||||
<div data-type="ollama">
|
||||
The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="caption_ollama_pull">click here</a>.
|
||||
</div>
|
||||
<label data-type="openai,anthropic,google" class="checkbox_label flexBasis100p" for="caption_allow_reverse_proxy" title="Allow using reverse proxy if defined and valid.">
|
||||
<input id="caption_allow_reverse_proxy" type="checkbox" class="checkbox">
|
||||
<span data-i18n="Allow reverse proxy">Allow reverse proxy</span>
|
||||
|
|
|
@ -148,7 +148,7 @@ export class SettingsUi {
|
|||
this.onQrSetChange();
|
||||
}
|
||||
onQrSetChange() {
|
||||
this.currentQrSet = QuickReplySet.get(this.currentSet.value);
|
||||
this.currentQrSet = QuickReplySet.get(this.currentSet.value) ?? new QuickReplySet();
|
||||
this.disableSend.checked = this.currentQrSet.disableSend;
|
||||
this.placeBeforeInput.checked = this.currentQrSet.placeBeforeInput;
|
||||
this.injectInput.checked = this.currentQrSet.injectInput;
|
||||
|
|
|
@ -20,7 +20,7 @@ import {
|
|||
renderExtensionTemplateAsync,
|
||||
doExtrasFetch, getApiUrl,
|
||||
} from '../../extensions.js';
|
||||
import { collapseNewlines } from '../../power-user.js';
|
||||
import { collapseNewlines, registerDebugFunction } from '../../power-user.js';
|
||||
import { SECRET_KEYS, secret_state, writeSecret } from '../../secrets.js';
|
||||
import { getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment } from '../../chats.js';
|
||||
import { debounce, getStringHash as calculateHash, waitUntilCondition, onlyUnique, splitRecursive, trimToStartSentence, trimToEndSentence } from '../../utils.js';
|
||||
|
@ -989,6 +989,28 @@ async function purgeVectorIndex(collectionId) {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Purges all vector indexes.
|
||||
*/
|
||||
async function purgeAllVectorIndexes() {
|
||||
try {
|
||||
const response = await fetch('/api/vector/purge-all', {
|
||||
method: 'POST',
|
||||
headers: getRequestHeaders(),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to purge all vector indexes');
|
||||
}
|
||||
|
||||
console.log('Vectors: Purged all vector indexes');
|
||||
toastr.success('All vector indexes purged', 'Purge successful');
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to purge all', error);
|
||||
toastr.error('Failed to purge all vector indexes', 'Purge failed');
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSettings() {
|
||||
$('#vectors_files_settings').toggle(!!settings.enabled_files);
|
||||
$('#vectors_chats_settings').toggle(!!settings.enabled_chats);
|
||||
|
@ -1502,6 +1524,13 @@ jQuery(async () => {
|
|||
saveSettingsDebounced();
|
||||
});
|
||||
|
||||
$('#vectors_ollama_pull').on('click', (e) => {
|
||||
const presetModel = extension_settings.vectors.ollama_model || '';
|
||||
e.preventDefault();
|
||||
$('#ollama_download_model').trigger('click');
|
||||
$('#dialogue_popup_input').val(presetModel);
|
||||
});
|
||||
|
||||
const validSecret = !!secret_state[SECRET_KEYS.NOMICAI];
|
||||
const placeholder = validSecret ? '✔️ Key saved' : '❌ Missing key';
|
||||
$('#api_key_nomicai').attr('placeholder', placeholder);
|
||||
|
@ -1571,4 +1600,11 @@ jQuery(async () => {
|
|||
],
|
||||
returns: ARGUMENT_TYPE.LIST,
|
||||
}));
|
||||
|
||||
registerDebugFunction('purge-everything', 'Purge all vector indices', 'Obliterate all stored vectors for all sources. No mercy.', async () => {
|
||||
if (!confirm('Are you sure?')) {
|
||||
return;
|
||||
}
|
||||
await purgeAllVectorIndexes();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -32,8 +32,11 @@
|
|||
<input id="vectors_ollama_keep" type="checkbox" />
|
||||
<span data-i18n="Keep model in memory">Keep model in memory</span>
|
||||
</label>
|
||||
<i data-i18n="Hint: Download models and set the URL in the API connection settings.">
|
||||
Hint: Download models and set the URL in the API connection settings.
|
||||
<div>
|
||||
The model must be downloaded first! Do it with the <code>ollama pull</code> command or <a href="#" id="vectors_ollama_pull">click here</a>.
|
||||
</div>
|
||||
<i data-i18n="Hint: Set the URL in the API connection settings.">
|
||||
Hint: Set the URL in the API connection settings.
|
||||
</i>
|
||||
</div>
|
||||
<div class="flex-container flexFlowColumn" id="llamacpp_vectorsModel">
|
||||
|
|
|
@ -3,31 +3,39 @@ import { removeFromArray, runAfterAnimation, uuidv4 } from './utils.js';
|
|||
/** @readonly */
|
||||
/** @enum {Number} */
|
||||
export const POPUP_TYPE = {
|
||||
'TEXT': 1,
|
||||
'CONFIRM': 2,
|
||||
'INPUT': 3,
|
||||
/** Main popup type. Containing any content displayed, with buttons below. Can also contain additional input controls. */
|
||||
TEXT: 1,
|
||||
/** Popup mainly made to confirm something, answering with a simple Yes/No or similar. Focus on the button controls. */
|
||||
CONFIRM: 2,
|
||||
/** Popup who's main focus is the input text field, which is displayed here. Can contain additional content above. Return value for this is the input string. */
|
||||
INPUT: 3,
|
||||
/** Popup without any button controls. Used to simply display content, with a small X in the corner. */
|
||||
DISPLAY: 4,
|
||||
};
|
||||
|
||||
/** @readonly */
|
||||
/** @enum {number?} */
|
||||
export const POPUP_RESULT = {
|
||||
'AFFIRMATIVE': 1,
|
||||
'NEGATIVE': 0,
|
||||
'CANCELLED': null,
|
||||
AFFIRMATIVE: 1,
|
||||
NEGATIVE: 0,
|
||||
CANCELLED: null,
|
||||
};
|
||||
|
||||
/**
|
||||
* @typedef {object} PopupOptions
|
||||
* @property {string|boolean?} [okButton] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
|
||||
* @property {string|boolean?} [cancelButton] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
|
||||
* @property {number?} [rows] - The number of rows for the input field
|
||||
* @property {boolean?} [wide] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
|
||||
* @property {boolean?} [wider] - Whether to display the popup in wider mode (just wider, no height scaling)
|
||||
* @property {boolean?} [large] - Whether to display the popup in large mode (90% of screen)
|
||||
* @property {boolean?} [allowHorizontalScrolling] - Whether to allow horizontal scrolling in the popup
|
||||
* @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 {string|boolean?} [okButton=null] - Custom text for the OK button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
|
||||
* @property {string|boolean?} [cancelButton=null] - Custom text for the Cancel button, or `true` to use the default (If set, the button will always be displayed, no matter the type of popup)
|
||||
* @property {number?} [rows=1] - The number of rows for the input field
|
||||
* @property {boolean?} [wide=false] - Whether to display the popup in wide mode (wide screen, 1/1 aspect ratio)
|
||||
* @property {boolean?} [wider=false] - Whether to display the popup in wider mode (just wider, no height scaling)
|
||||
* @property {boolean?} [large=false] - Whether to display the popup in large mode (90% of screen)
|
||||
* @property {boolean?} [transparent=false] - Whether to display the popup in transparent mode (no background, border, shadow or anything, only its content)
|
||||
* @property {boolean?} [allowHorizontalScrolling=false] - Whether to allow horizontal scrolling in the popup
|
||||
* @property {boolean?} [allowVerticalScrolling=false] - Whether to allow vertical scrolling in the popup
|
||||
* @property {POPUP_RESULT|number?} [defaultResult=POPUP_RESULT.AFFIRMATIVE] - The default result of this popup when Enter is pressed. Can be changed from `POPUP_RESULT.AFFIRMATIVE`.
|
||||
* @property {CustomPopupButton[]|string[]?} [customButtons=null] - 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=null] - Handler called before the popup closes, return `false` to cancel the close
|
||||
* @property {(popup: Popup) => void?} [onClose=null] - Handler called after the popup closes, but before the DOM is cleaned up
|
||||
*/
|
||||
|
||||
/**
|
||||
|
@ -73,11 +81,15 @@ export class Popup {
|
|||
/** @type {HTMLElement} */ content;
|
||||
/** @type {HTMLTextAreaElement} */ input;
|
||||
/** @type {HTMLElement} */ controls;
|
||||
/** @type {HTMLElement} */ ok;
|
||||
/** @type {HTMLElement} */ cancel;
|
||||
/** @type {HTMLElement} */ okButton;
|
||||
/** @type {HTMLElement} */ cancelButton;
|
||||
/** @type {HTMLElement} */ closeButton;
|
||||
/** @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 +106,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, transparent = 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
|
||||
|
@ -109,19 +125,21 @@ export class Popup {
|
|||
this.content = this.dlg.querySelector('.popup-content');
|
||||
this.input = this.dlg.querySelector('.popup-input');
|
||||
this.controls = this.dlg.querySelector('.popup-controls');
|
||||
this.ok = this.dlg.querySelector('.popup-button-ok');
|
||||
this.cancel = this.dlg.querySelector('.popup-button-cancel');
|
||||
this.okButton = this.dlg.querySelector('.popup-button-ok');
|
||||
this.cancelButton = this.dlg.querySelector('.popup-button-cancel');
|
||||
this.closeButton = this.dlg.querySelector('.popup-button-close');
|
||||
|
||||
this.dlg.setAttribute('data-id', this.id);
|
||||
if (wide) this.dlg.classList.add('wide_dialogue_popup');
|
||||
if (wider) this.dlg.classList.add('wider_dialogue_popup');
|
||||
if (large) this.dlg.classList.add('large_dialogue_popup');
|
||||
if (transparent) this.dlg.classList.add('transparent_dialogue_popup');
|
||||
if (allowHorizontalScrolling) this.dlg.classList.add('horizontal_scrolling_dialogue_popup');
|
||||
if (allowVerticalScrolling) this.dlg.classList.add('vertical_scrolling_dialogue_popup');
|
||||
|
||||
// If custom button captions are provided, we set them beforehand
|
||||
this.ok.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
this.cancel.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
|
||||
this.okButton.textContent = typeof okButton === 'string' ? okButton : 'OK';
|
||||
this.cancelButton.textContent = typeof cancelButton === 'string' ? cancelButton : template.getAttribute('popup-button-cancel');
|
||||
|
||||
this.defaultResult = defaultResult;
|
||||
this.customButtons = customButtons;
|
||||
|
@ -132,17 +150,14 @@ export class Popup {
|
|||
const buttonElement = document.createElement('div');
|
||||
buttonElement.classList.add('menu_button', 'popup-button-custom', 'result-control');
|
||||
buttonElement.classList.add(...(button.classes ?? []));
|
||||
buttonElement.setAttribute('data-result', String(button.result ?? undefined));
|
||||
buttonElement.dataset.result = String(button.result ?? undefined);
|
||||
buttonElement.textContent = button.text;
|
||||
buttonElement.tabIndex = 0;
|
||||
|
||||
if (button.action) buttonElement.addEventListener('click', button.action);
|
||||
if (button.result) buttonElement.addEventListener('click', () => this.complete(button.result));
|
||||
|
||||
if (button.appendAtEnd) {
|
||||
this.controls.appendChild(buttonElement);
|
||||
} else {
|
||||
this.controls.insertBefore(buttonElement, this.ok);
|
||||
this.controls.insertBefore(buttonElement, this.okButton);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -150,23 +165,30 @@ export class Popup {
|
|||
const defaultButton = this.controls.querySelector(`[data-result="${this.defaultResult}"]`);
|
||||
if (defaultButton) defaultButton.classList.add('menu_button_default');
|
||||
|
||||
// Styling differences depending on the popup type
|
||||
// General styling for all types first, that might be overriden for specific types below
|
||||
this.input.style.display = 'none';
|
||||
this.closeButton.style.display = 'none';
|
||||
|
||||
switch (type) {
|
||||
case POPUP_TYPE.TEXT: {
|
||||
this.input.style.display = 'none';
|
||||
if (!cancelButton) this.cancel.style.display = 'none';
|
||||
if (!cancelButton) this.cancelButton.style.display = 'none';
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.CONFIRM: {
|
||||
this.input.style.display = 'none';
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup-button-yes');
|
||||
if (!cancelButton) this.cancel.textContent = template.getAttribute('popup-button-no');
|
||||
if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-yes');
|
||||
if (!cancelButton) this.cancelButton.textContent = template.getAttribute('popup-button-no');
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.INPUT: {
|
||||
this.input.style.display = 'block';
|
||||
if (!okButton) this.ok.textContent = template.getAttribute('popup-button-save');
|
||||
if (!okButton) this.okButton.textContent = template.getAttribute('popup-button-save');
|
||||
break;
|
||||
}
|
||||
case POPUP_TYPE.DISPLAY: {
|
||||
this.controls.style.display = 'none';
|
||||
this.closeButton.style.display = 'block';
|
||||
}
|
||||
default: {
|
||||
console.warn('Unknown popup type.', type);
|
||||
break;
|
||||
|
@ -193,8 +215,14 @@ export class Popup {
|
|||
// Set focus event that remembers the focused element
|
||||
this.dlg.addEventListener('focusin', (evt) => { if (evt.target instanceof HTMLElement && evt.target != this.dlg) this.lastFocus = evt.target; });
|
||||
|
||||
this.ok.addEventListener('click', () => this.complete(POPUP_RESULT.AFFIRMATIVE));
|
||||
this.cancel.addEventListener('click', () => this.complete(POPUP_RESULT.NEGATIVE));
|
||||
// Bind event listeners for all result controls to their defined event type
|
||||
this.dlg.querySelectorAll(`[data-result]`).forEach(resultControl => {
|
||||
if (!(resultControl instanceof HTMLElement)) return;
|
||||
const result = Number(resultControl.dataset.result);
|
||||
if (isNaN(result)) throw new Error('Invalid result control. Result must be a number. ' + resultControl.dataset.result);
|
||||
const type = resultControl.dataset.resultEvent || 'click';
|
||||
resultControl.addEventListener(type, () => this.complete(result));
|
||||
});
|
||||
|
||||
// Bind dialog listeners manually, so we can be sure context is preserved
|
||||
const cancelListener = (evt) => {
|
||||
|
@ -287,6 +315,9 @@ export class Popup {
|
|||
|
||||
if (applyAutoFocus) {
|
||||
control.setAttribute('autofocus', '');
|
||||
// Manually enable tabindex too, as this might only be applied by the interactable functionality in the background, but too late for HTML autofocus
|
||||
// interactable only gets applied when inserted into the DOM
|
||||
control.tabIndex = 0;
|
||||
} else {
|
||||
control.focus();
|
||||
}
|
||||
|
@ -317,6 +348,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 +374,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();
|
||||
|
||||
|
|
|
@ -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';
|
||||
|
@ -199,6 +199,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,
|
||||
|
||||
|
@ -1563,6 +1564,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);
|
||||
|
@ -1591,7 +1598,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);
|
||||
|
@ -1633,6 +1639,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');
|
||||
|
@ -3517,11 +3524,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();
|
||||
|
@ -3760,6 +3762,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();
|
||||
|
|
|
@ -142,7 +142,10 @@ class PresetManager {
|
|||
* @param {string} value Preset option value
|
||||
*/
|
||||
selectPreset(value) {
|
||||
$(this.select).find(`option[value=${value}]`).prop('selected', true);
|
||||
const option = $(this.select).filter(function() {
|
||||
return $(this).val() === value;
|
||||
});
|
||||
option.prop('selected', true);
|
||||
$(this.select).val(value).trigger('change');
|
||||
}
|
||||
|
||||
|
|
|
@ -2,7 +2,6 @@ import {
|
|||
characters,
|
||||
saveSettingsDebounced,
|
||||
this_chid,
|
||||
callPopup,
|
||||
menu_type,
|
||||
entitiesFilter,
|
||||
printCharactersDebounced,
|
||||
|
@ -23,12 +22,13 @@ 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 { commonEnumProviders } from './slash-commands/SlashCommandCommonEnumsProvider.js';
|
||||
import { renderTemplateAsync } from './templates.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
|
@ -65,11 +65,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
|
||||
|
@ -245,16 +254,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;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -286,11 +302,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];
|
||||
|
@ -303,6 +320,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);
|
||||
|
@ -684,15 +702,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;
|
||||
|
||||
|
@ -700,11 +709,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;
|
||||
|
@ -722,9 +733,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))
|
||||
|
@ -732,17 +745,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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -752,63 +770,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 => `"${button.text}"`);
|
||||
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 "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask"."
|
||||
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 "Tag Import Option".\n\nIf the "Import" option is chosen, the global setting will stay on "Ask".">
|
||||
</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 [];
|
||||
|
@ -1137,8 +1147,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)
|
||||
|
@ -1157,9 +1167,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 } });
|
||||
}
|
||||
|
||||
|
|
|
@ -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.<br />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
If anything besides 'Cancel' is selected, this dialog will not show up anymore.
To change this, go to the settings and modify "Tag Import Option".

If the "Import" option is chosen, the global setting will stay on "Ask"."
|
||||
title="Remember the chosen import option
If anything besides 'Cancel' is selected, this dialog will not show up anymore.
To change this, go to the settings and modify "Tag Import Option".

If the "Import" option is chosen, the global setting will stay on "Ask".">
|
||||
</div>
|
||||
</span>
|
||||
</label>
|
||||
</small>
|
||||
</div>
|
|
@ -21,9 +21,11 @@
|
|||
<li><tt>{{char_version}}</tt> – <span data-i18n="help_macros_17">the Character's version number</span></li>
|
||||
<li><tt>{{group}}</tt> – <span data-i18n="help_macros_18">a comma-separated list of group member names or the character name in solo chats. Alias: {{charIfNotGroup}}</span></li>
|
||||
<li><tt>{{model}}</tt> – <span data-i18n="help_macros_19">a text generation model name for the currently selected API. </span><b data-i18n="Can be inaccurate!">Can be inaccurate!</b></li>
|
||||
<li><tt>{{lastMessage}}</tt> - <span data-i18n="help_macros_20">the text of the latest chat message.</span></li>
|
||||
<li><tt>{{lastMessage}}</tt> – <span data-i18n="help_macros_20">the text of the latest chat message.</span></li>
|
||||
<li><tt>{{lastUserMessage}}</tt> – <span data-i18n="help_macros_lastUser">the text of the latest user chat message.</span></li>
|
||||
<li><tt>{{lastCharMessage}}</tt> – <span data-i18n="help_macros_lastChar">the text of the latest character chat message.</span></li>
|
||||
<li><tt>{{lastMessageId}}</tt> – <span data-i18n="help_macros_21">index # of the latest chat message. Useful for slash command batching.</span></li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> - <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
|
||||
<li><tt>{{firstIncludedMessageId}}</tt> – <span data-i18n="help_macros_22">the ID of the first message included in the context. Requires generation to be ran at least once in the current session.</span></li>
|
||||
<li><tt>{{currentSwipeId}}</tt> – <span data-i18n="help_macros_23">the 1-based ID of the current swipe in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{lastSwipeId}}</tt> – <span data-i18n="help_macros_24">the number of swipes in the last chat message. Empty string if the last message is user or prompt-hidden.</span></li>
|
||||
<li><tt>{{// (note)}}</tt> – <span data-i18n="help_macros_25">you can leave a note here, and the macro will be replaced with blank content. Not visible for the AI.</span></li>
|
||||
|
|
|
@ -39,6 +39,8 @@ const OPENROUTER_PROVIDERS = [
|
|||
'Novita',
|
||||
'Lynn',
|
||||
'Lynn 2',
|
||||
'DeepSeek',
|
||||
'Infermatic',
|
||||
];
|
||||
|
||||
export async function loadOllamaModels(data) {
|
||||
|
|
107
public/style.css
107
public/style.css
|
@ -366,11 +366,6 @@ input[type='checkbox']:focus-visible {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
.img_enlarged_container {
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
.img_enlarged_container pre code,
|
||||
.mes_text pre code {
|
||||
position: relative;
|
||||
display: block;
|
||||
|
@ -2380,6 +2375,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;
|
||||
|
@ -3152,6 +3151,16 @@ grammarly-extension {
|
|||
min-width: 750px;
|
||||
}
|
||||
|
||||
.transparent_dialogue_popup {
|
||||
background-color: transparent;
|
||||
box-shadow: none;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.transparent_dialogue_popup:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
#dialogue_popup .horizontal_scrolling_dialogue_popup {
|
||||
overflow-x: unset !important;
|
||||
}
|
||||
|
@ -4465,38 +4474,106 @@ a {
|
|||
|
||||
.mes_img_controls {
|
||||
position: absolute;
|
||||
top: 0.5em;
|
||||
top: 0.1em;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
display: none;
|
||||
display: flex;
|
||||
opacity: 0;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.mes_img_controls .right_menu_button {
|
||||
padding: 0;
|
||||
filter: brightness(80%);
|
||||
padding: 1px;
|
||||
height: 1.25em;
|
||||
width: 1.25em;
|
||||
}
|
||||
|
||||
.mes_img_controls .right_menu_button::before {
|
||||
/* Fix weird alignment with this font-awesome icons on focus */
|
||||
position: relative;
|
||||
top: 0.6125em;
|
||||
}
|
||||
|
||||
.mes_img_controls .right_menu_button:hover {
|
||||
filter: brightness(150%);
|
||||
}
|
||||
|
||||
.mes_img_container:hover .mes_img_controls {
|
||||
display: flex;
|
||||
.mes_img_container:hover .mes_img_controls,
|
||||
.mes_img_container:focus-within .mes_img_controls {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.mes .mes_img_container.img_extra {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.img_enlarged_holder {
|
||||
/* Scaling via flex-grow and object-fit only works if we have some kind of base-height set */
|
||||
min-height: 120px;
|
||||
}
|
||||
|
||||
.img_enlarged_holder:has(.zoomed) {
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.img_enlarged {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 2px;
|
||||
border: 1px solid transparent;
|
||||
outline: 1px solid var(--SmartThemeBorderColor);
|
||||
object-fit: contain;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
cursor: zoom-in
|
||||
}
|
||||
|
||||
.img_enlarged.zoomed {
|
||||
object-fit: cover;
|
||||
width: auto;
|
||||
height: auto;
|
||||
cursor: zoom-out;
|
||||
}
|
||||
|
||||
.img_enlarged_container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: center;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.img_enlarged_holder::-webkit-scrollbar-corner {
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
.img_enlarged_container pre code {
|
||||
position: relative;
|
||||
display: block;
|
||||
overflow-x: auto;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.popup:has(.img_enlarged.zoomed).large_dialogue_popup {
|
||||
height: 100vh !important;
|
||||
height: 100svh !important;
|
||||
max-height: 100vh !important;
|
||||
max-height: 100svh !important;
|
||||
max-width: 100vw !important;
|
||||
max-width: 100svw !important;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-content {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .img_enlarged_container pre {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.popup:has(.img_enlarged.zoomed).large_dialogue_popup .popup-button-close {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.cropper-container {
|
||||
|
|
|
@ -191,7 +191,7 @@ router.post('/delete', jsonParser, async (request, response) => {
|
|||
return response.status(400).send('Bad Request: extensionName is required in the request body.');
|
||||
}
|
||||
|
||||
// Sanatize the extension name to prevent directory traversal
|
||||
// Sanitize the extension name to prevent directory traversal
|
||||
const extensionName = sanitize(request.body.extensionName);
|
||||
|
||||
try {
|
||||
|
@ -201,7 +201,7 @@ router.post('/delete', jsonParser, async (request, response) => {
|
|||
return response.status(404).send(`Directory does not exist at ${extensionPath}`);
|
||||
}
|
||||
|
||||
await fs.promises.rmdir(extensionPath, { recursive: true });
|
||||
await fs.promises.rm(extensionPath, { recursive: true });
|
||||
console.log(`Extension has been deleted at ${extensionPath}`);
|
||||
|
||||
return response.send(`Extension has been deleted at ${extensionPath}`);
|
||||
|
|
|
@ -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') {
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -1,5 +1,6 @@
|
|||
const vectra = require('vectra');
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const express = require('express');
|
||||
const sanitize = require('sanitize-filename');
|
||||
const { jsonParser } = require('../express-common');
|
||||
|
@ -440,6 +441,24 @@ router.post('/delete', jsonParser, async (req, res) => {
|
|||
}
|
||||
});
|
||||
|
||||
router.post('/purge-all', jsonParser, async (req, res) => {
|
||||
try {
|
||||
for (const source of SOURCES) {
|
||||
const sourcePath = path.join(req.user.directories.vectors, sanitize(source));
|
||||
if (!fs.existsSync(sourcePath)) {
|
||||
continue;
|
||||
}
|
||||
await fs.promises.rm(sourcePath, { recursive: true });
|
||||
console.log(`Deleted vector source store at ${sourcePath}`);
|
||||
}
|
||||
|
||||
return res.sendStatus(200);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
return res.sendStatus(500);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/purge', jsonParser, async (req, res) => {
|
||||
try {
|
||||
if (!req.body.collectionId) {
|
||||
|
|
Loading…
Reference in New Issue