Compare commits
30 Commits
efa6cdb07e
...
23b5059429
Author | SHA1 | Date |
---|---|---|
Juha Jeronen | 23b5059429 | |
Cohee | 4e9a113a35 | |
LenAnderson | 70a35e9b49 | |
LenAnderson | bc4a8fbe1d | |
LenAnderson | d77a70b25a | |
LenAnderson | e49317a73c | |
LenAnderson | 87cc28ae28 | |
Cohee | b93131ec7a | |
Cohee | 4219468e20 | |
Cohee | 4227968dfa | |
Cohee | 09790bb994 | |
Matěj Račinský | bd1bfee941 | |
Cohee | 0b3c49da90 | |
Cohee | c3d6e10795 | |
Cohee | 1eae9bd18b | |
Cohee | 99e24f5588 | |
Cohee | 38e2bf955c | |
Cohee | 0653dad5c5 | |
Cohee | 51af830db8 | |
Cohee | 2bde9d2b15 | |
Wolfsblvt | 96a9f7108c | |
Juha Jeronen | 54c1c5912f | |
Juha Jeronen | 6874e361ae | |
Juha Jeronen | 820dbc97a5 | |
Juha Jeronen | 7c0b944d81 | |
Juha Jeronen | e59a337303 | |
Juha Jeronen | 4102705a8e | |
Juha Jeronen | 7432e00bb6 | |
Juha Jeronen | 82dbfa9ac5 | |
Juha Jeronen | 7672b5260c |
|
@ -103,7 +103,8 @@
|
|||
}
|
||||
|
||||
#bulkTagsList,
|
||||
#tagList .tag {
|
||||
#tagList .tag,
|
||||
#groupTagList .tag {
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
|
|
|
@ -1458,7 +1458,7 @@
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby" class="wide100p">
|
||||
<div data-newbie-hidden id="json_schema_block" data-tg-type="tabby, llamacpp" class="wide100p">
|
||||
<hr class="wide100p">
|
||||
<h4 class="wide100p textAlignCenter"><span data-i18n="JSON Schema">JSON Schema</span>
|
||||
<a href="https://json-schema.org/learn/getting-started-step-by-step" target="_blank">
|
||||
|
|
|
@ -1020,12 +1020,12 @@ function parseLlmResponse(emotionResponse, labels) {
|
|||
const parsedEmotion = JSON.parse(emotionResponse);
|
||||
return parsedEmotion?.emotion ?? fallbackExpression;
|
||||
} catch {
|
||||
const fuse = new Fuse([emotionResponse]);
|
||||
for (const label of labels) {
|
||||
const result = fuse.search(label);
|
||||
if (result.length > 0) {
|
||||
return label;
|
||||
}
|
||||
const fuse = new Fuse(labels, { includeScore: true });
|
||||
console.debug('Using fuzzy search in labels:', labels);
|
||||
const result = fuse.search(emotionResponse);
|
||||
if (result.length > 0) {
|
||||
console.debug(`fuzzy search found: ${result[0].item} as closest for the LLM response:`, emotionResponse);
|
||||
return result[0].item;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -232,6 +232,7 @@
|
|||
}
|
||||
body .dialogue_popup:has(#qr--modalEditor) .dialogue_popup_text > #qr--modalEditor > #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
.dialogue_popup:has(#qr--modalEditor) {
|
||||
|
|
|
@ -255,6 +255,7 @@
|
|||
}
|
||||
> #qr--main > .qr--modal-messageContainer > #qr--modal-messageHolder {
|
||||
min-height: 50svh;
|
||||
height: 50svh;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -55,6 +55,7 @@ const settings = {
|
|||
|
||||
// For files
|
||||
enabled_files: false,
|
||||
science_mode: false,
|
||||
translate_files: false,
|
||||
size_threshold: 10,
|
||||
chunk_size: 5000,
|
||||
|
@ -95,7 +96,7 @@ async function onVectorizeAllClick() {
|
|||
const chatId = getCurrentChatId();
|
||||
|
||||
if (!chatId) {
|
||||
toastr.info('No chat selected', 'Vectorization aborted');
|
||||
toastr.info('No chat selected. Vectorization aborted.', 'Vector Storage');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -108,7 +109,7 @@ async function onVectorizeAllClick() {
|
|||
|
||||
while (!finished) {
|
||||
if (is_send_press) {
|
||||
toastr.info('Message generation is in progress.', 'Vectorization aborted');
|
||||
toastr.info('Message generation is in progress. Vectorization aborted.', 'Vector Storage');
|
||||
throw new Error('Message generation is in progress.');
|
||||
}
|
||||
|
||||
|
@ -135,6 +136,7 @@ async function onVectorizeAllClick() {
|
|||
}
|
||||
} catch (error) {
|
||||
console.error('Vectors: Failed to vectorize all', error);
|
||||
toastr.error(`Vectorize all failed. ${new String(error)}`, 'Vector Storage')
|
||||
} finally {
|
||||
$('#vectorize_progress').hide();
|
||||
}
|
||||
|
@ -274,14 +276,14 @@ async function synchronizeChat(batchSize = 5) {
|
|||
case 'extras_module_missing':
|
||||
return 'Extras API must provide an "embeddings" module.';
|
||||
default:
|
||||
return 'Check server console for more details';
|
||||
return 'Check server console for more details.';
|
||||
}
|
||||
}
|
||||
|
||||
console.error('Vectors: Failed to synchronize chat', error);
|
||||
|
||||
const message = getErrorMessage(error.cause);
|
||||
toastr.error(message, 'Vectorization failed', { preventDuplicates: true });
|
||||
toastr.error(`Vectorization failed. ${message}`, 'Vector Storage', { preventDuplicates: true });
|
||||
return -1;
|
||||
} finally {
|
||||
syncBlocked = false;
|
||||
|
@ -357,6 +359,7 @@ async function processFiles(chat) {
|
|||
if (!message?.extra?.file) {
|
||||
continue;
|
||||
}
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: has a file attachment, processing.`)
|
||||
|
||||
// Trim file inserted by the script
|
||||
const fileText = String(message.mes)
|
||||
|
@ -367,6 +370,7 @@ async function processFiles(chat) {
|
|||
|
||||
// File is too small
|
||||
if (fileText.length < thresholdLength) {
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: text of file "${message.extra.file.name}" shorter than vectorization threshold (${fileText.length} < ${thresholdLength} chars), keeping inlined.`)
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -379,11 +383,16 @@ async function processFiles(chat) {
|
|||
|
||||
// File is already in the collection
|
||||
if (!hashesInCollection.length) {
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: file "${fileName}" not yet in collection, vectorizing.`)
|
||||
await vectorizeFile(fileText, fileName, collectionId, settings.chunk_size);
|
||||
} else {
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: file "${fileName}" found in collection.`)
|
||||
}
|
||||
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: querying vector DB.`)
|
||||
const queryText = await getQueryText(chat);
|
||||
const fileChunks = await retrieveFileChunks(queryText, collectionId);
|
||||
console.debug(`Vectors: processFiles: message ${message.index}: retrieved ${fileChunks.length} chars.`);
|
||||
|
||||
message.mes = `${fileChunks}\n\n${message.mes}`;
|
||||
}
|
||||
|
@ -438,6 +447,62 @@ async function retrieveFileChunks(queryText, collectionId) {
|
|||
return fileText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitizes the text content of a scientific paper to obtain higher-quality text for vectorization.
|
||||
*
|
||||
* This is a really simplistic, classical regex-based algorithm. An LLM could likely do better, but that would be slow.
|
||||
* We hope to get a result that's not horribly broken and that won't include irrelevant RAG query poisoning stuff.
|
||||
*
|
||||
* Currently, we:
|
||||
*
|
||||
* - Strip the reference list.
|
||||
*
|
||||
* The reference list contains the highest concentration of keywords of any kind (in the titles of the cited studies),
|
||||
* so it usually poisons RAG queries so that no matter what you search for, you'll only get chunks of the reference list.
|
||||
* Omitting the reference list from the text to be vectorized, RAG will look for matches in the paper content only.
|
||||
*
|
||||
* - F IX H EADINGS T HAT L OOK L IKE T HIS.
|
||||
*
|
||||
* This is a rather common issue in text extraction from a PDF.
|
||||
*
|
||||
* @param {string} fileText The text to sanitize
|
||||
* @returns {string} The sanitized text
|
||||
*/
|
||||
function sanitizeScientificInput(fileText) {
|
||||
// Fix section headings
|
||||
//
|
||||
const brokenUppercaseWordsFinder = new RegExp(/(?<!\b[A-Z]\s+)\b([A-Z])\s+([A-Z]+)\b/, 'g'); // "H EADING", but not "C H EADING" (appendix section)
|
||||
fileText = fileText.replaceAll(brokenUppercaseWordsFinder, '$1$2');
|
||||
const brokenAppendixHeadingFinder = new RegExp(/([A-Z])\s+([A-Z])\s+([A-Z]+)\b/, 'g'); // "C H EADING"
|
||||
fileText = fileText.replaceAll(brokenAppendixHeadingFinder, '$1 $2$3'); // -> "C HEADING"
|
||||
|
||||
const brokenHeadingsFinder = new RegExp(/^\s*([A-Z])\s+([a-z]+)\s*$/, 'mg'); // "H eading", on its own line
|
||||
fileText = fileText.replaceAll(brokenHeadingsFinder, '$1$2');
|
||||
|
||||
// Strip reference list (easier now that the headings are already fixed).
|
||||
//
|
||||
// Linefeeds are sometimes lost, so the references may begin in the middle of a line.
|
||||
// Since we can't trigger on any random mention of the word "References", we trigger in the middle of a line
|
||||
// only for an all-uppercase "REFERENCES".
|
||||
//
|
||||
const referencesFinder = new RegExp(/(^\s*References\s*$|^\s*REFERENCES\s*$|\bREFERENCES\s*)/, 'mg');
|
||||
const referencesMatches = [...fileText.matchAll(referencesFinder)];
|
||||
if (referencesMatches.length > 0) { // Detected a reference list
|
||||
const appendixFinder = new RegExp(/(^\s*Appendi(x|ces)\s*$|^\s*A\s*PPENDI(X|CES)\s*$|\bAPPENDI(X|CES)\s*)/, 'mg');
|
||||
// Some documents just start appendices like "A Some stuff..." without a heading, but there's not much we can do about that.
|
||||
// In those cases, we will simply ignore the appendices.
|
||||
const appendixMatches = [...fileText.matchAll(appendixFinder)];
|
||||
if (appendixMatches.length > 0) { // Detected both a reference list and appendices
|
||||
fileText = fileText.substring(0, referencesMatches[0].index).trim() + fileText.substring(appendixMatches[0].index);
|
||||
} else { // Detected only a reference list, no appendices
|
||||
fileText = fileText.substring(0, referencesMatches[0].index).trim();
|
||||
}
|
||||
}
|
||||
|
||||
console.debug(fileText);
|
||||
return fileText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vectorizes a file and inserts it into the vector index.
|
||||
* @param {string} fileText File text
|
||||
|
@ -454,12 +519,19 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize) {
|
|||
fileText = translatedText;
|
||||
}
|
||||
|
||||
const toast = toastr.info('Vectorization may take some time, please wait...', `Ingesting file ${fileName}`);
|
||||
const chunks = splitRecursive(fileText, chunkSize);
|
||||
const toast = toastr.info(`Ingesting file ${fileName}. Vectorization may take some time, please wait...`, 'Vector Storage');
|
||||
|
||||
if (settings.science_mode) {
|
||||
console.debug(`Vectors: Science mode is enabled. Sanitizing input ${fileName}.`);
|
||||
fileText = sanitizeScientificInput(fileText);
|
||||
}
|
||||
|
||||
const chunks = splitRecursive(fileText, settings.chunk_size);
|
||||
console.debug(`Vectors: Split file ${fileName} into ${chunks.length} chunks`, chunks);
|
||||
|
||||
const items = chunks.map((chunk, index) => ({ hash: getStringHash(chunk), text: chunk, index: index }));
|
||||
await insertVectorItems(collectionId, items);
|
||||
toastr.info(`Vectorization complete for ${fileName}.`, `Vector Storage`);
|
||||
|
||||
toastr.clear(toast);
|
||||
console.log(`Vectors: Inserted ${chunks.length} vector items for file ${fileName} into ${collectionId}`);
|
||||
|
@ -467,6 +539,7 @@ async function vectorizeFile(fileText, fileName, collectionId, chunkSize) {
|
|||
} catch (error) {
|
||||
toastr.error(String(error), 'Failed to vectorize file', { preventDuplicates: true });
|
||||
console.error('Vectors: Failed to vectorize file', error);
|
||||
toastr.error(`Vectorization failed for ${fileName}. ${new String(error)}`, 'Vector Storage');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -873,20 +946,20 @@ function toggleSettings() {
|
|||
async function onPurgeClick() {
|
||||
const chatId = getCurrentChatId();
|
||||
if (!chatId) {
|
||||
toastr.info('No chat selected', 'Purge aborted');
|
||||
toastr.info('No chat selected. Purge aborted.', 'Vector Storage');
|
||||
return;
|
||||
}
|
||||
if (await purgeVectorIndex(chatId)) {
|
||||
toastr.success('Vector index purged', 'Purge successful');
|
||||
toastr.success('Vector index purged successfully.', 'Vector Storage');
|
||||
} else {
|
||||
toastr.error('Failed to purge vector index', 'Purge failed');
|
||||
toastr.error('Failed to purge vector index', 'Vector Storage');
|
||||
}
|
||||
}
|
||||
|
||||
async function onViewStatsClick() {
|
||||
const chatId = getCurrentChatId();
|
||||
if (!chatId) {
|
||||
toastr.info('No chat selected');
|
||||
toastr.info('No chat selected', 'Vector Storage');
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -1120,6 +1193,11 @@ jQuery(async () => {
|
|||
saveSettingsDebounced();
|
||||
toggleSettings();
|
||||
});
|
||||
$('#vectors_science_mode').prop('checked', settings.science_mode).on('input', () => {
|
||||
settings.science_mode = $('#vectors_science_mode').prop('checked');
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
saveSettingsDebounced();
|
||||
});
|
||||
$('#vectors_source').val(settings.source).on('change', () => {
|
||||
settings.source = String($('#vectors_source').val());
|
||||
Object.assign(extension_settings.vectors, settings);
|
||||
|
|
|
@ -200,6 +200,10 @@
|
|||
<input id="vectors_chunk_count_db" type="number" class="text_pole widthUnset" min="1" max="99999" />
|
||||
</div>
|
||||
</div>
|
||||
<label class="checkbox_label" for="vectors_science_mode" title="Sanitize input text to improve retrieval quality for scientific paper inputs.">
|
||||
<input id="vectors_science_mode" type="checkbox" class="checkbox">
|
||||
Science mode
|
||||
</label>
|
||||
<div class="flex-container flexFlowColumn">
|
||||
<label for="vectors_file_template_db">
|
||||
<span>Injection Template</span>
|
||||
|
|
|
@ -114,7 +114,9 @@ class PresetManager {
|
|||
* @returns {any} Preset value
|
||||
*/
|
||||
findPreset(name) {
|
||||
return $(this.select).find(`option:contains(${name})`).val();
|
||||
return $(this.select).find('option').filter(function() {
|
||||
return $(this).text() === name;
|
||||
}).val();
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -704,6 +704,19 @@ SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'addswipe',
|
|||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'abort',
|
||||
callback: abortCallback,
|
||||
namedArgumentList: [
|
||||
SlashCommandNamedArgument.fromProps({ name: 'quiet',
|
||||
description: 'Whether to suppress the toast message notifying about the /abort call.',
|
||||
typeList: [ARGUMENT_TYPE.BOOLEAN],
|
||||
defaultValue: 'true',
|
||||
enumList: ['true', 'false'],
|
||||
}),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
SlashCommandArgument.fromProps({ description: 'The reason for aborting command execution. Shown when quiet=false',
|
||||
typeList: [ARGUMENT_TYPE.STRING],
|
||||
}),
|
||||
],
|
||||
helpString: 'Aborts the slash command batch execution.',
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({ name: 'fuzzy',
|
||||
|
@ -1421,9 +1434,15 @@ async function runCallback(args, name) {
|
|||
}
|
||||
}
|
||||
|
||||
function abortCallback() {
|
||||
$('#send_textarea').val('')[0].dispatchEvent(new Event('input', { bubbles:true }));
|
||||
throw new Error('/abort command executed', { cause: 'abort' });
|
||||
/**
|
||||
*
|
||||
* @param {object} param0
|
||||
* @param {SlashCommandAbortController} param0._abortController
|
||||
* @param {string} [param0.quiet]
|
||||
* @param {string} [reason]
|
||||
*/
|
||||
function abortCallback({ _abortController, quiet }, reason) {
|
||||
_abortController.abort((reason ?? '').toString().length == 0 ? '/abort command executed' : reason, !isFalseBoolean(quiet ?? 'true'));
|
||||
}
|
||||
|
||||
async function delayCallback(_, amount) {
|
||||
|
@ -2786,7 +2805,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
|||
|
||||
let closure;
|
||||
try {
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController);
|
||||
closure = parser.parse(text, true, options.parserFlags, options.abortController ?? new SlashCommandAbortController());
|
||||
closure.scope.parent = options.scope;
|
||||
closure.onProgress = options.onProgress;
|
||||
} catch (e) {
|
||||
|
@ -2813,7 +2832,7 @@ async function executeSlashCommandsWithOptions(text, options = {}) {
|
|||
|
||||
try {
|
||||
const result = await closure.execute();
|
||||
if (result.isAborted) {
|
||||
if (result.isAborted && !result.isQuietlyAborted) {
|
||||
toastr.warning(result.abortReason, 'Command execution aborted');
|
||||
}
|
||||
return result;
|
||||
|
|
|
@ -1,5 +1,25 @@
|
|||
import { SlashCommandAbortController } from './SlashCommandAbortController.js';
|
||||
import { SlashCommandArgument, SlashCommandNamedArgument } from './SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './SlashCommandClosure.js';
|
||||
import { PARSER_FLAG } from './SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './SlashCommandScope.js';
|
||||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* @typedef {{
|
||||
* _pipe:string|SlashCommandClosure,
|
||||
* _scope:SlashCommandScope,
|
||||
* _parserFlags:{[id:PARSER_FLAG]:boolean},
|
||||
* _abortController:SlashCommandAbortController,
|
||||
* [id:string]:string,
|
||||
* }} NamedArguments
|
||||
*/
|
||||
|
||||
/**
|
||||
* @typedef {string|SlashCommandClosure|(string|SlashCommandClosure)[]} UnnamedArguments
|
||||
*/
|
||||
|
||||
|
||||
|
||||
|
@ -8,7 +28,7 @@ export class SlashCommand {
|
|||
* Creates a SlashCommand from a properties object.
|
||||
* @param {Object} props
|
||||
* @param {string} [props.name]
|
||||
* @param {(namedArguments:Object.<string,string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {(namedArguments:NamedArguments, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|void|Promise<string|SlashCommandClosure|void>} [props.callback]
|
||||
* @param {string} [props.helpString]
|
||||
* @param {boolean} [props.splitUnnamedArgument]
|
||||
* @param {string[]} [props.aliases]
|
||||
|
@ -25,7 +45,7 @@ export class SlashCommand {
|
|||
|
||||
|
||||
/**@type {string}*/ name;
|
||||
/**@type {(namedArguments:Object<string, string|SlashCommandClosure>, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {(namedArguments:{_pipe:string|SlashCommandClosure, _scope:SlashCommandScope, _abortController:SlashCommandAbortController, [id:string]:string|SlashCommandClosure}, unnamedArguments:string|SlashCommandClosure|(string|SlashCommandClosure)[])=>string|SlashCommandClosure|Promise<string|SlashCommandClosure>}*/ callback;
|
||||
/**@type {string}*/ helpString;
|
||||
/**@type {boolean}*/ splitUnnamedArgument = false;
|
||||
/**@type {string[]}*/ aliases = [];
|
||||
|
|
|
@ -5,7 +5,8 @@ export class SlashCommandAbortController {
|
|||
constructor() {
|
||||
this.signal = new SlashCommandAbortSignal();
|
||||
}
|
||||
abort(reason = 'No reason.') {
|
||||
abort(reason = 'No reason.', isQuiet = false) {
|
||||
this.signal.isQuiet = isQuiet;
|
||||
this.signal.aborted = true;
|
||||
this.signal.reason = reason;
|
||||
}
|
||||
|
@ -20,8 +21,8 @@ export class SlashCommandAbortController {
|
|||
}
|
||||
|
||||
export class SlashCommandAbortSignal {
|
||||
/**@type {boolean}*/ isQuiet = false;
|
||||
/**@type {boolean}*/ paused = false;
|
||||
/**@type {boolean}*/ aborted = false;
|
||||
/**@type {string}*/ reason = null;
|
||||
|
||||
}
|
||||
|
|
|
@ -161,6 +161,7 @@ export class SlashCommandClosure {
|
|||
let args = {
|
||||
_scope: this.scope,
|
||||
_parserFlags: executor.parserFlags,
|
||||
_abortController: this.abortController,
|
||||
};
|
||||
let value;
|
||||
// substitute named arguments
|
||||
|
@ -254,6 +255,7 @@ export class SlashCommandClosure {
|
|||
if (this.abortController?.signal?.aborted) {
|
||||
const result = new SlashCommandClosureResult();
|
||||
result.isAborted = true;
|
||||
result.isQuietlyAborted = this.abortController.signal.isQuiet;
|
||||
result.abortReason = this.abortController.signal.reason.toString();
|
||||
return result;
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ export class SlashCommandClosureResult {
|
|||
/**@type {boolean}*/ interrupt = false;
|
||||
/**@type {string}*/ pipe;
|
||||
/**@type {boolean}*/ isAborted = false;
|
||||
/**@type {boolean}*/ isQuietlyAborted = false;
|
||||
/**@type {string}*/ abortReason;
|
||||
/**@type {boolean}*/ isError = false;
|
||||
/**@type {string}*/ errorMessage;
|
||||
|
|
|
@ -20,7 +20,7 @@ export class SlashCommandExecutor {
|
|||
// @ts-ignore
|
||||
/**@type {SlashCommandNamedArgumentAssignment[]}*/ namedArgumentList = [];
|
||||
/**@type {SlashCommandUnnamedArgumentAssignment[]}*/ unnamedArgumentList = [];
|
||||
/**@type {Object<PARSER_FLAG,boolean>} */ parserFlags;
|
||||
/**@type {{[id:PARSER_FLAG]:boolean}} */ parserFlags;
|
||||
|
||||
get commandCount() {
|
||||
return 1
|
||||
|
|
|
@ -829,6 +829,7 @@ export class SlashCommandParser {
|
|||
assignment.start = this.index;
|
||||
value = '';
|
||||
}
|
||||
assignment.start = this.index;
|
||||
assignment.value = this.parseClosure();
|
||||
assignment.end = this.index;
|
||||
listValues.push(assignment);
|
||||
|
|
|
@ -14,10 +14,12 @@ import {
|
|||
// eslint-disable-next-line no-unused-vars
|
||||
import { FILTER_TYPES, FILTER_STATES, DEFAULT_FILTER_STATE, isFilterState, FilterHelper } from './filters.js';
|
||||
|
||||
import { groupCandidatesFilter, groups, selected_group } from './group-chats.js';
|
||||
import { groupCandidatesFilter, groups, select_group_chats, selected_group } from './group-chats.js';
|
||||
import { download, onlyUnique, parseJsonFile, uuidv4, getSortableDelay, flashHighlight } from './utils.js';
|
||||
import { power_user } from './power-user.js';
|
||||
import { debounce_timeout } from './constants.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
|
||||
export {
|
||||
TAG_FOLDER_TYPES,
|
||||
|
@ -357,7 +359,7 @@ function createTagMapFromList(listElement, key) {
|
|||
* If you have an entity, you can get it's key easily via `getTagKeyForEntity(entity)`.
|
||||
*
|
||||
* @param {string} key - The key for which to get tags via the tag map
|
||||
* @param {boolean} [sort=true] -
|
||||
* @param {boolean} [sort=true] - Whether the tag list should be sorted
|
||||
* @returns {Tag[]} A list of tags
|
||||
*/
|
||||
function getTagsList(key, sort = true) {
|
||||
|
@ -463,35 +465,122 @@ export function getTagKeyForEntityElement(element) {
|
|||
return undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag to a given entity
|
||||
* @param {Tag} tag - The tag to add
|
||||
* @param {string|string[]} entityId - The entity to add this tag to. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the new tag too (for example because the add was triggered for that function)
|
||||
* @param {PrintTagListOptions} [options.tagListOptions] - Optional parameters for printing the tag list. Can be set to be consistent with the expected behavior of tags in the list that was defined before.
|
||||
* @returns {boolean} Whether at least one tag was added
|
||||
*/
|
||||
export function addTagToEntity(tag, entityId, { tagListSelector = null, tagListOptions = {} } = {}) {
|
||||
let result = false;
|
||||
// Add tags to the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = addTagToMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = addTagToMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
if (tagListSelector) printTagList(tagListSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given entity
|
||||
* @param {Tag} tag - The tag to remove
|
||||
* @param {string|string[]} entityId - The entity to remove this tag from. Has to be the entity key (e.g. `addTagToEntity`). (Also allows multiple entities to be passed in)
|
||||
* @param {object} [options={}] - Optional arguments
|
||||
* @param {JQuery<HTMLElement>|string?} [options.tagListSelector=null] - An optional selector if a specific list should be updated with the tag removed too (for example because the add was triggered for that function)
|
||||
* @param {JQuery<HTMLElement>?} [options.tagElement=null] - Optionally a direct html element of the tag to be removed, so it can be removed from the UI
|
||||
* @returns {boolean} Whether at least one tag was removed
|
||||
*/
|
||||
export function removeTagFromEntity(tag, entityId, { tagListSelector = null, tagElement = null } = {}) {
|
||||
let result = false;
|
||||
// Remove tag from the map
|
||||
if (Array.isArray(entityId)) {
|
||||
entityId.forEach((id) => result = removeTagFromMap(tag.id, id) || result);
|
||||
} else {
|
||||
result = removeTagFromMap(tag.id, entityId);
|
||||
}
|
||||
|
||||
// Save and redraw
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We don't reprint the lists, we can just remove the html elements from them.
|
||||
if (tagListSelector) {
|
||||
const $selector = (typeof tagListSelector === 'string') ? $(tagListSelector) : tagListSelector;
|
||||
$selector.find(`.tag[id="${tag.id}"]`).remove();
|
||||
}
|
||||
if (tagElement) tagElement.remove();
|
||||
$(`${getInlineListSelector()} .tag[id="${tag.id}"]`).remove();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a tag from a given character. If no character is provided, adds it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was added or not
|
||||
*/
|
||||
function addTagToMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [tagId];
|
||||
return true;
|
||||
}
|
||||
else {
|
||||
if (tag_map[key].includes(tagId))
|
||||
return false;
|
||||
|
||||
tag_map[key].push(tagId);
|
||||
tag_map[key] = tag_map[key].filter(onlyUnique);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a tag from a given character. If no character is provided, removes it from the currently active one.
|
||||
* @param {string} tagId - The id of the tag
|
||||
* @param {string} characterId - The id/key of the character or group
|
||||
* @returns {boolean} Whether the tag was removed or not
|
||||
*/
|
||||
function removeTagFromMap(tagId, characterId = null) {
|
||||
const key = characterId !== null && characterId !== undefined ? getTagKeyForEntity(characterId) : getTagKey();
|
||||
|
||||
if (!key) {
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Array.isArray(tag_map[key])) {
|
||||
tag_map[key] = [];
|
||||
return false;
|
||||
}
|
||||
else {
|
||||
const indexOf = tag_map[key].indexOf(tagId);
|
||||
tag_map[key].splice(indexOf, 1);
|
||||
return indexOf !== -1;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -535,24 +624,7 @@ function selectTag(event, ui, listSelector, { tagListOptions = {} } = {}) {
|
|||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => addTagToMap(tag.id, characterId));
|
||||
} else {
|
||||
addTagToMap(tag.id);
|
||||
}
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
// We should manually add the selected tag to the print tag function, so we cover places where the tag list did not automatically include it
|
||||
tagListOptions.addTag = tag;
|
||||
|
||||
// add tag to the UI and internal map - we reprint so sorting and new markup is done correctly
|
||||
printTagList(listSelector, tagListOptions);
|
||||
const inlineSelector = getInlineListSelector();
|
||||
if (inlineSelector) {
|
||||
printTagList($(inlineSelector), tagListOptions);
|
||||
}
|
||||
addTagToEntity(tag, characterIds, { tagListSelector: listSelector, tagListOptions: tagListOptions });
|
||||
|
||||
// need to return false to keep the input clear
|
||||
return false;
|
||||
|
@ -635,6 +707,7 @@ function createNewTag(tagName) {
|
|||
create_date: Date.now(),
|
||||
};
|
||||
tags.push(tag);
|
||||
console.debug('Created new tag', tag.name, 'with id', tag.id);
|
||||
return tag;
|
||||
}
|
||||
|
||||
|
@ -932,8 +1005,8 @@ function updateTagFilterIndicator() {
|
|||
|
||||
function onTagRemoveClick(event) {
|
||||
event.stopPropagation();
|
||||
const tag = $(this).closest('.tag');
|
||||
const tagId = tag.attr('id');
|
||||
const tagElement = $(this).closest('.tag');
|
||||
const tagId = tagElement.attr('id');
|
||||
|
||||
// Check if we are inside the drilldown. If so, we call remove on the bogus folder
|
||||
if ($(this).closest('.rm_tag_bogus_drilldown').length > 0) {
|
||||
|
@ -942,24 +1015,13 @@ function onTagRemoveClick(event) {
|
|||
return;
|
||||
}
|
||||
|
||||
const tag = tags.find(t => t.id === tagId);
|
||||
|
||||
// Optional, check for multiple character ids being present.
|
||||
const characterData = event.target.closest('#bulk_tags_div')?.dataset.characters;
|
||||
const characterIds = characterData ? JSON.parse(characterData).characterIds : null;
|
||||
|
||||
tag.remove();
|
||||
|
||||
if (characterIds) {
|
||||
characterIds.forEach((characterId) => removeTagFromMap(tagId, characterId));
|
||||
} else {
|
||||
removeTagFromMap(tagId);
|
||||
}
|
||||
|
||||
$(`${getInlineListSelector()} .tag[id="${tagId}"]`).remove();
|
||||
|
||||
printCharactersDebounced();
|
||||
saveSettingsDebounced();
|
||||
|
||||
|
||||
removeTagFromEntity(tag, characterIds, { tagElement: tagElement });
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
|
@ -985,7 +1047,7 @@ function onGroupCreateClick() {
|
|||
|
||||
export function applyTagsOnCharacterSelect() {
|
||||
//clearTagsFilter();
|
||||
const chid = Number($(this).attr('chid'));
|
||||
const chid = Number(this_chid);
|
||||
printTagList($('#tagList'), { forEntityOrKey: chid, tagOptions: { removable: true } });
|
||||
}
|
||||
|
||||
|
@ -1461,14 +1523,200 @@ function printViewTagList(empty = true) {
|
|||
}
|
||||
}
|
||||
|
||||
function registerTagsSlashCommands() {
|
||||
/**
|
||||
* Gets the key for char/group for a slash command. If none can be found, a toastr will be shown and null returned.
|
||||
* @param {string?} [charName] The optionally provided char name
|
||||
* @returns {string?} - The char/group key, or null if none found
|
||||
*/
|
||||
function paraGetCharKey(charName) {
|
||||
const entity = charName
|
||||
? (characters.find(x => x.name === charName) || groups.find(x => x.name == charName))
|
||||
: (selected_group ? groups.find(x => x.id == selected_group) : characters[this_chid]);
|
||||
const key = getTagKeyForEntity(entity);
|
||||
if (!key) {
|
||||
toastr.warning(`Character ${charName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return key;
|
||||
}
|
||||
/**
|
||||
* Gets a tag by its name. Optionally can create the tag if it does not exist.
|
||||
* @param {string} tagName - The name of the tag
|
||||
* @param {object} options - Optional arguments
|
||||
* @param {boolean} [options.allowCreate=false] - Whether a new tag should be created if no tag with the name exists
|
||||
* @returns {Tag?} The tag, or null if not found
|
||||
*/
|
||||
function paraGetTag(tagName, { allowCreate = false } = {}) {
|
||||
if (!tagName) {
|
||||
toastr.warning('Tag name must be provided.');
|
||||
return null;
|
||||
}
|
||||
let tag = tags.find(t => t.name === tagName);
|
||||
if (allowCreate && !tag) {
|
||||
tag = createNewTag(tagName);
|
||||
}
|
||||
if (!tag) {
|
||||
toastr.warning(`Tag ${tagName} not found.`);
|
||||
return null;
|
||||
}
|
||||
return tag;
|
||||
}
|
||||
|
||||
function updateTagsList() {
|
||||
switch (menu_type) {
|
||||
case 'characters':
|
||||
printTagFilters(tag_filter_types.character);
|
||||
printTagFilters(tag_filter_types.group_member);
|
||||
break;
|
||||
case 'character_edit':
|
||||
applyTagsOnCharacterSelect();
|
||||
break;
|
||||
case 'group_edit':
|
||||
select_group_chats(selected_group, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-add',
|
||||
returns: 'true/false - Whether the tag was added or was assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName, { allowCreate: true });
|
||||
if (!tag) return 'false';
|
||||
const result = addTagToEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Adds a tag to the character. If no character is provided, it adds it to the current character (<code>{{char}}</code>).
|
||||
If the tag doesn't exist, it is created.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-add name="Chloe" scenario</code></pre>
|
||||
will add the tag "scenario" to the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-remove',
|
||||
returns: 'true/false - Whether the tag was removed or wasn\'t assigned already',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
const result = removeTagFromEntity(tag, key);
|
||||
updateTagsList();
|
||||
return String(result);
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Removes a tag from the character. If no character is provided, it removes it from the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-remove name="Chloe" scenario</code></pre>
|
||||
will remove the tag "scenario" from the character named Chloe.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-exists',
|
||||
returns: 'true/false - Whether the given tag name is assigned to the character',
|
||||
/** @param {{name: string}} namedArgs @param {string} tagName @returns {string} */
|
||||
callback: ({ name }, tagName) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return 'false';
|
||||
const tag = paraGetTag(tagName);
|
||||
if (!tag) return 'false';
|
||||
return String(tag_map[key].includes(tag.id));
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
unnamedArgumentList: [
|
||||
new SlashCommandArgument('tag name', [ARGUMENT_TYPE.STRING], true),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Checks whether the given tag is assigned to the character. If no character is provided, it checks the current character (<code>{{char}}</code>).
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-exists name="Chloe" scenario</code></pre>
|
||||
will return true if the character named Chloe has the tag "scenario".
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
SlashCommandParser.addCommandObject(SlashCommand.fromProps({
|
||||
name: 'tag-list',
|
||||
returns: 'Comma-separated list of all assigned tags',
|
||||
/** @param {{name: string}} namedArgs @returns {string} */
|
||||
callback: ({ name }) => {
|
||||
const key = paraGetCharKey(name);
|
||||
if (!key) return '';
|
||||
const tags = getTagsList(key);
|
||||
return tags.map(x => x.name).join(', ');
|
||||
},
|
||||
namedArgumentList: [
|
||||
new SlashCommandNamedArgument('name', 'Character name', [ARGUMENT_TYPE.STRING], false, false, '{{char}}'),
|
||||
],
|
||||
helpString: `
|
||||
<div>
|
||||
Lists all assigned tags of the character. If no character is provided, it uses the current character (<code>{{char}}</code>).
|
||||
<br />
|
||||
Note that there is no special handling for tags containing commas, they will be printed as-is.
|
||||
</div>
|
||||
<div>
|
||||
<strong>Example:</strong>
|
||||
<ul>
|
||||
<li>
|
||||
<pre><code>/tag-list name="Chloe"</code></pre>
|
||||
could return something like <code>OC, scenario, edited, funny</code>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
`,
|
||||
}));
|
||||
}
|
||||
|
||||
export function initTags() {
|
||||
createTagInput('#tagInput', '#tagList', { tagOptions: { removable: true } });
|
||||
createTagInput('#groupTagInput', '#groupTagList', { tagOptions: { removable: true } });
|
||||
|
||||
$(document).on('click', '#rm_button_create', onCharacterCreateClick);
|
||||
$(document).on('click', '#rm_button_group_chats', onGroupCreateClick);
|
||||
$(document).on('click', '.character_select', applyTagsOnCharacterSelect);
|
||||
$(document).on('click', '.group_select', applyTagsOnGroupSelect);
|
||||
$(document).on('click', '.tag_remove', onTagRemoveClick);
|
||||
$(document).on('input', '.tag_input', onTagInput);
|
||||
$(document).on('click', '.tags_view', onViewTagsListClick);
|
||||
|
@ -1479,6 +1727,7 @@ export function initTags() {
|
|||
$(document).on('click', '.tag_view_backup', onTagsBackupClick);
|
||||
$(document).on('click', '.tag_view_restore', onBackupRestoreClick);
|
||||
eventSource.on(event_types.CHARACTER_DUPLICATED, copyTags);
|
||||
eventSource.makeFirst(event_types.CHAT_CHANGED, () => selected_group ? applyTagsOnGroupSelect() : applyTagsOnCharacterSelect());
|
||||
|
||||
$(document).on('input', '#dialogue_popup input[name="auto_sort_tags"]', (evt) => {
|
||||
const toggle = $(evt.target).is(':checked');
|
||||
|
@ -1506,4 +1755,6 @@ export function initTags() {
|
|||
printCharactersDebounced();
|
||||
}
|
||||
}
|
||||
|
||||
registerTagsSlashCommands();
|
||||
}
|
||||
|
|
|
@ -991,7 +991,7 @@ export function getTextGenModel() {
|
|||
}
|
||||
|
||||
export function isJsonSchemaSupported() {
|
||||
return settings.type === TABBY && main_api === 'textgenerationwebui';
|
||||
return [TABBY, LLAMACPP].includes(settings.type) && main_api === 'textgenerationwebui';
|
||||
}
|
||||
|
||||
export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate, isContinue, cfgValues, type) {
|
||||
|
@ -1065,7 +1065,7 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
|||
'guidance_scale': cfgValues?.guidanceScale?.value ?? settings.guidance_scale ?? 1,
|
||||
'negative_prompt': cfgValues?.negativePrompt ?? substituteParams(settings.negative_prompt) ?? '',
|
||||
'grammar_string': settings.grammar_string,
|
||||
'json_schema': settings.type === TABBY ? settings.json_schema : undefined,
|
||||
'json_schema': [TABBY, LLAMACPP].includes(settings.type) ? settings.json_schema : undefined,
|
||||
// llama.cpp aliases. In case someone wants to use LM Studio as Text Completion API
|
||||
'repeat_penalty': settings.rep_pen,
|
||||
'tfs_z': settings.tfs,
|
||||
|
@ -1150,5 +1150,15 @@ export function getTextGenGenerationData(finalPrompt, maxTokens, isImpersonate,
|
|||
|
||||
eventSource.emitAndWait(event_types.TEXT_COMPLETION_SETTINGS_READY, params);
|
||||
|
||||
// Grammar conflicts with with json_schema
|
||||
if (settings.type === LLAMACPP) {
|
||||
if (params.json_schema && Object.keys(params.json_schema).length > 0) {
|
||||
delete params.grammar_string;
|
||||
delete params.grammar;
|
||||
} else {
|
||||
delete params.json_schema;
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
}
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
import { chat_metadata, getCurrentChatId, saveSettingsDebounced, sendSystemMessage, system_message_types } from '../script.js';
|
||||
import { extension_settings, saveMetadataDebounced } from './extensions.js';
|
||||
import { executeSlashCommands } from './slash-commands.js';
|
||||
import { executeSlashCommands, executeSlashCommandsWithOptions } from './slash-commands.js';
|
||||
import { SlashCommand } from './slash-commands/SlashCommand.js';
|
||||
import { SlashCommandAbortController } from './slash-commands/SlashCommandAbortController.js';
|
||||
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from './slash-commands/SlashCommandArgument.js';
|
||||
import { SlashCommandClosure } from './slash-commands/SlashCommandClosure.js';
|
||||
import { SlashCommandClosureResult } from './slash-commands/SlashCommandClosureResult.js';
|
||||
import { SlashCommandEnumValue } from './slash-commands/SlashCommandEnumValue.js';
|
||||
import { SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { PARSER_FLAG, SlashCommandParser } from './slash-commands/SlashCommandParser.js';
|
||||
import { SlashCommandScope } from './slash-commands/SlashCommandScope.js';
|
||||
import { isFalseBoolean } from './utils.js';
|
||||
|
||||
|
@ -317,58 +319,101 @@ function listVariablesCallback() {
|
|||
async function whileCallback(args, command) {
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS;
|
||||
if (command) {
|
||||
if (command[0] instanceof SlashCommandClosure) {
|
||||
command = command[0];
|
||||
} else {
|
||||
command = command.join(' ');
|
||||
}
|
||||
}
|
||||
|
||||
let commandResult;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) await command.execute();
|
||||
else await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
commandResult = await command.execute();
|
||||
} else {
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags, args._abortController);
|
||||
if (commandResult.isAborted) {
|
||||
args._abortController.abort(commandResult.abortReason, true);
|
||||
}
|
||||
}
|
||||
break;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
return commandResult.pipe;
|
||||
}
|
||||
|
||||
return '';
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {import('./slash-commands/SlashCommand.js').NamedArguments} args
|
||||
* @param {import('./slash-commands/SlashCommand.js').UnnamedArguments} value
|
||||
* @returns
|
||||
*/
|
||||
async function timesCallback(args, value) {
|
||||
let repeats;
|
||||
let command;
|
||||
if (Array.isArray(value)) {
|
||||
[repeats, command] = value;
|
||||
[repeats, ...command] = value;
|
||||
if (command[0] instanceof SlashCommandClosure) {
|
||||
command = command[0];
|
||||
} else {
|
||||
command = command.join(' ');
|
||||
}
|
||||
} else {
|
||||
[repeats, ...command] = value.split(' ');
|
||||
[repeats, ...command] = /**@type {string}*/(value).split(' ');
|
||||
command = command.join(' ');
|
||||
}
|
||||
const isGuardOff = isFalseBoolean(args.guard);
|
||||
const iterations = Math.min(Number(repeats), isGuardOff ? Number.MAX_SAFE_INTEGER : MAX_LOOPS);
|
||||
let result;
|
||||
for (let i = 0; i < iterations; i++) {
|
||||
/**@type {SlashCommandClosureResult}*/
|
||||
if (command instanceof SlashCommandClosure) {
|
||||
command.scope.setMacro('timesIndex', i);
|
||||
await command.execute();
|
||||
result = await command.execute();
|
||||
}
|
||||
else {
|
||||
await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i), args._scope, args._parserFlags);
|
||||
result = await executeSubCommands(command.replace(/\{\{timesIndex\}\}/g, i.toString()), args._scope, args._parserFlags);
|
||||
if (result.isAborted) {
|
||||
args._abortController.abort(result.abortReason, true);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return '';
|
||||
return result?.pipe ?? '';
|
||||
}
|
||||
|
||||
async function ifCallback(args, command) {
|
||||
const { a, b, rule } = parseBooleanOperands(args);
|
||||
const result = evalBoolean(rule, a, b);
|
||||
|
||||
let commandResult;
|
||||
if (result && command) {
|
||||
if (command instanceof SlashCommandClosure) return (await command.execute()).pipe;
|
||||
return await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
commandResult = await executeSubCommands(command, args._scope, args._parserFlags);
|
||||
} else if (!result && args.else && ((typeof args.else === 'string' && args.else !== '') || args.else instanceof SlashCommandClosure)) {
|
||||
if (args.else instanceof SlashCommandClosure) return (await args.else.execute(args._scope)).pipe;
|
||||
return await executeSubCommands(args.else, args._scope, args._parserFlags);
|
||||
commandResult = await executeSubCommands(args.else, args._scope, args._parserFlags);
|
||||
}
|
||||
|
||||
if (commandResult) {
|
||||
if (commandResult.isAborted) {
|
||||
args._abortController.abort(commandResult.abortReason, true);
|
||||
}
|
||||
return commandResult.pipe;
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
|
@ -511,20 +556,25 @@ function evalBoolean(rule, a, b) {
|
|||
/**
|
||||
* Executes a slash command from a string (may be enclosed in quotes) and returns the result.
|
||||
* @param {string} command Command to execute. May contain escaped macro and batch separators.
|
||||
* @returns {Promise<string>} Pipe result
|
||||
* @param {SlashCommandScope} [scope] The scope to use.
|
||||
* @param {PARSER_FLAG[]} [parserFlags] The parser flags to use.
|
||||
* @returns {Promise<SlashCommandClosureResult>} Closure execution result
|
||||
*/
|
||||
async function executeSubCommands(command, scope = null, parserFlags = null) {
|
||||
if (command.startsWith('"') && command.endsWith('"')) {
|
||||
command = command.slice(1, -1);
|
||||
}
|
||||
|
||||
const result = await executeSlashCommands(command, true, scope, false, parserFlags);
|
||||
const abortController = new SlashCommandAbortController();
|
||||
const result = await executeSlashCommandsWithOptions(command, {
|
||||
handleExecutionErrors: false,
|
||||
handleParserErrors: false,
|
||||
parserFlags,
|
||||
scope,
|
||||
abortController,
|
||||
});
|
||||
|
||||
if (!result || typeof result !== 'object') {
|
||||
return '';
|
||||
}
|
||||
|
||||
return result?.pipe || '';
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1132,6 +1182,7 @@ export function registerVariableCommands() {
|
|||
'command to execute while true', [ARGUMENT_TYPE.CLOSURE, ARGUMENT_TYPE.SUBCOMMAND], true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Compares the value of the left operand <code>a</code> with the value of the right operand <code>b</code>,
|
||||
|
@ -1184,6 +1235,7 @@ export function registerVariableCommands() {
|
|||
true,
|
||||
),
|
||||
],
|
||||
splitUnnamedArgument: true,
|
||||
helpString: `
|
||||
<div>
|
||||
Execute any valid slash command enclosed in quotes <code>repeats</code> number of times.
|
||||
|
|
|
@ -2068,6 +2068,7 @@ input[type="file"] {
|
|||
gap: 5px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.bulk_select_checkbox {
|
||||
|
@ -4892,4 +4893,4 @@ body:not(.movingUI) .drawer-content.maximized {
|
|||
.regex-operator { color: #FFB6C1; } /* Light Pink */
|
||||
.regex-flags { color: #98FB98; } /* Pale Green */
|
||||
.regex-delimiter { font-weight: bold; color: #FF6961; } /* Pastel Red */
|
||||
.regex-highlight { color: #FAF8F6; } /* Pastel White */
|
||||
.regex-highlight { color: #FAF8F6; } /* Pastel White */
|
||||
|
|
Loading…
Reference in New Issue