mirror of
				https://github.com/SillyTavern/SillyTavern.git
				synced 2025-06-05 21:59:27 +02:00 
			
		
		
		
	
		
			
				
	
	
		
			382 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
		
			15 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
import { event_types, eventSource, saveSettingsDebounced } from '../../../script.js';
 | 
						|
import { deleteAttachment, getDataBankAttachments, getDataBankAttachmentsForSource, getFileAttachment, uploadFileAttachmentToServer } from '../../chats.js';
 | 
						|
import { extension_settings, renderExtensionTemplateAsync } from '../../extensions.js';
 | 
						|
import { SlashCommand } from '../../slash-commands/SlashCommand.js';
 | 
						|
import { ARGUMENT_TYPE, SlashCommandArgument, SlashCommandNamedArgument } from '../../slash-commands/SlashCommandArgument.js';
 | 
						|
import { SlashCommandClosure } from '../../slash-commands/SlashCommandClosure.js';
 | 
						|
import { enumIcons } from '../../slash-commands/SlashCommandCommonEnumsProvider.js';
 | 
						|
import { SlashCommandEnumValue, enumTypes } from '../../slash-commands/SlashCommandEnumValue.js';
 | 
						|
import { SlashCommandExecutor } from '../../slash-commands/SlashCommandExecutor.js';
 | 
						|
import { SlashCommandParser } from '../../slash-commands/SlashCommandParser.js';
 | 
						|
 | 
						|
/**
 | 
						|
 * List of attachment sources
 | 
						|
 * @type {string[]}
 | 
						|
 */
 | 
						|
const TYPES = ['global', 'character', 'chat'];
 | 
						|
const FIELDS = ['name', 'url'];
 | 
						|
 | 
						|
/**
 | 
						|
 * Get attachments from the data bank. Includes disabled attachments.
 | 
						|
 * @param {string} [source] Source for the attachments
 | 
						|
 * @returns {import('../../chats').FileAttachment[]} List of attachments
 | 
						|
 */
 | 
						|
function getAttachments(source) {
 | 
						|
    if (!source || !TYPES.includes(source)) {
 | 
						|
        return getDataBankAttachments(true);
 | 
						|
    }
 | 
						|
 | 
						|
    return getDataBankAttachmentsForSource(source, true);
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get attachment by a single name or URL.
 | 
						|
 * @param {import('../../chats').FileAttachment[]} attachments List of attachments
 | 
						|
 * @param {string} value Name or URL of the attachment
 | 
						|
 * @returns {import('../../chats').FileAttachment} Attachment
 | 
						|
 */
 | 
						|
function getAttachmentByField(attachments, value) {
 | 
						|
    const match = (a) => String(a).trim().toLowerCase() === String(value).trim().toLowerCase();
 | 
						|
    const fullMatchByURL = attachments.find(it => match(it.url));
 | 
						|
    const fullMatchByName = attachments.find(it => match(it.name));
 | 
						|
    return fullMatchByURL || fullMatchByName;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Get attachment by multiple fields.
 | 
						|
 * @param {import('../../chats').FileAttachment[]} attachments List of attachments
 | 
						|
 * @param {string[]} values Name and URL of the attachment to search for
 | 
						|
 * @returns
 | 
						|
 */
 | 
						|
function getAttachmentByFields(attachments, values) {
 | 
						|
    for (const value of values) {
 | 
						|
        const attachment = getAttachmentByField(attachments, value);
 | 
						|
        if (attachment) {
 | 
						|
            return attachment;
 | 
						|
        }
 | 
						|
    }
 | 
						|
 | 
						|
    return null;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for listing attachments in the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @returns {string} JSON string of the list of attachments
 | 
						|
 */
 | 
						|
function listDataBankAttachments(args) {
 | 
						|
    const attachments = getAttachments(args?.source);
 | 
						|
    const field = args?.field;
 | 
						|
    return JSON.stringify(attachments.map(a => FIELDS.includes(field) ? a[field] : a.url));
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for getting text from an attachment in the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Name or URL of the attachment
 | 
						|
 * @returns {Promise<string>} Content of the attachment
 | 
						|
 */
 | 
						|
async function getDataBankText(args, value) {
 | 
						|
    if (!value) {
 | 
						|
        toastr.warning('No attachment name or URL provided.');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const attachments = getAttachments(args?.source);
 | 
						|
    const attachment = getAttachmentByField(attachments, value);
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
        toastr.warning('Attachment not found.');
 | 
						|
        return;
 | 
						|
    }
 | 
						|
 | 
						|
    const content = await getFileAttachment(attachment.url);
 | 
						|
    return content;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for adding an attachment to the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Content of the attachment
 | 
						|
 * @returns {Promise<string>} URL of the attachment
 | 
						|
 */
 | 
						|
async function uploadDataBankAttachment(args, value) {
 | 
						|
    const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
 | 
						|
    const name = args?.name || new Date().toLocaleString();
 | 
						|
    const file = new File([value], name, { type: 'text/plain' });
 | 
						|
    const url = await uploadFileAttachmentToServer(file, source);
 | 
						|
    return url;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for updating an attachment in the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Content of the attachment
 | 
						|
 * @returns {Promise<string>} URL of the attachment
 | 
						|
 */
 | 
						|
async function updateDataBankAttachment(args, value) {
 | 
						|
    const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
 | 
						|
    const attachments = getAttachments(source);
 | 
						|
    const attachment = getAttachmentByFields(attachments, [args?.url, args?.name]);
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
        toastr.warning('Attachment not found.');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    await deleteAttachment(attachment, source, () => { }, false);
 | 
						|
    const file = new File([value], attachment.name, { type: 'text/plain' });
 | 
						|
    const url = await uploadFileAttachmentToServer(file, source);
 | 
						|
    return url;
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for deleting an attachment from the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Name or URL of the attachment
 | 
						|
 * @returns {Promise<string>} Empty string
 | 
						|
 */
 | 
						|
async function deleteDataBankAttachment(args, value) {
 | 
						|
    const source = args?.source && TYPES.includes(args.source) ? args.source : 'chat';
 | 
						|
    const attachments = getAttachments(source);
 | 
						|
    const attachment = getAttachmentByField(attachments, value);
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
        toastr.warning('Attachment not found.');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    await deleteAttachment(attachment, source, () => { }, false);
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for disabling an attachment in the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Name or URL of the attachment
 | 
						|
 * @returns {Promise<string>} Empty string
 | 
						|
 */
 | 
						|
async function disableDataBankAttachment(args, value) {
 | 
						|
    const attachments = getAttachments(args?.source);
 | 
						|
    const attachment = getAttachmentByField(attachments, value);
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
        toastr.warning('Attachment not found.');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    if (extension_settings.disabled_attachments.includes(attachment.url)) {
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    extension_settings.disabled_attachments.push(attachment.url);
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
/**
 | 
						|
 * Callback for enabling an attachment in the data bank.
 | 
						|
 * @param {object} args Named arguments
 | 
						|
 * @param {string} value Name or URL of the attachment
 | 
						|
 * @returns {Promise<string>} Empty string
 | 
						|
 */
 | 
						|
async function enableDataBankAttachment(args, value) {
 | 
						|
    const attachments = getAttachments(args?.source);
 | 
						|
    const attachment = getAttachmentByField(attachments, value);
 | 
						|
 | 
						|
    if (!attachment) {
 | 
						|
        toastr.warning('Attachment not found.');
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    const index = extension_settings.disabled_attachments.indexOf(attachment.url);
 | 
						|
    if (index === -1) {
 | 
						|
        return '';
 | 
						|
    }
 | 
						|
 | 
						|
    extension_settings.disabled_attachments.splice(index, 1);
 | 
						|
    return '';
 | 
						|
}
 | 
						|
 | 
						|
function cleanUpAttachments() {
 | 
						|
    let shouldSaveSettings = false;
 | 
						|
    if (extension_settings.character_attachments) {
 | 
						|
        Object.values(extension_settings.character_attachments).flat().filter(a => a.text).forEach(a => {
 | 
						|
            shouldSaveSettings = true;
 | 
						|
            delete a.text;
 | 
						|
        });
 | 
						|
    }
 | 
						|
    if (Array.isArray(extension_settings.attachments)) {
 | 
						|
        extension_settings.attachments.filter(a => a.text).forEach(a => {
 | 
						|
            shouldSaveSettings = true;
 | 
						|
            delete a.text;
 | 
						|
        });
 | 
						|
    }
 | 
						|
    if (shouldSaveSettings) {
 | 
						|
        saveSettingsDebounced();
 | 
						|
    }
 | 
						|
}
 | 
						|
 | 
						|
jQuery(async () => {
 | 
						|
    eventSource.on(event_types.APP_READY, cleanUpAttachments);
 | 
						|
    const manageButton = await renderExtensionTemplateAsync('attachments', 'manage-button', {});
 | 
						|
    const attachButton = await renderExtensionTemplateAsync('attachments', 'attach-button', {});
 | 
						|
    $('#data_bank_wand_container').append(manageButton);
 | 
						|
    $('#attach_file_wand_container').append(attachButton);
 | 
						|
 | 
						|
    /** A collection of local enum providers for this context of data bank */
 | 
						|
    const localEnumProviders = {
 | 
						|
        /**
 | 
						|
         * All attachments in the data bank based on the source argument. If not provided, defaults to 'chat'.
 | 
						|
         * @param {'name' | 'url'} returnField - Whether the enum should return the 'name' field or the 'url'
 | 
						|
         * @param {'chat' | 'character' | 'global' | ''} fallbackSource - The source to use if the source argument is not provided. Empty string to use all sources.
 | 
						|
         * */
 | 
						|
        attachments: (returnField = 'name', fallbackSource = 'chat') => (/** @type {SlashCommandExecutor} */ executor) => {
 | 
						|
            const source = executor.namedArgumentList.find(it => it.name == 'source')?.value ?? fallbackSource;
 | 
						|
            if (source instanceof SlashCommandClosure) throw new Error('Argument \'source\' does not support closures');
 | 
						|
            const attachments = getAttachments(source);
 | 
						|
 | 
						|
            return attachments.map(attachment => new SlashCommandEnumValue(
 | 
						|
                returnField === 'name' ? attachment.name : attachment.url,
 | 
						|
                `${enumIcons.getStateIcon(!extension_settings.disabled_attachments.includes(attachment.url))} [${source}] ${returnField === 'url' ? attachment.name : attachment.url}`,
 | 
						|
                enumTypes.enum, enumIcons.file));
 | 
						|
        },
 | 
						|
    };
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db',
 | 
						|
        callback: () => {
 | 
						|
            document.getElementById('manageAttachments')?.click();
 | 
						|
            return '';
 | 
						|
        },
 | 
						|
        aliases: ['databank', 'data-bank'],
 | 
						|
        helpString: 'Open the data bank',
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-list',
 | 
						|
        callback: listDataBankAttachments,
 | 
						|
        aliases: ['databank-list', 'data-bank-list'],
 | 
						|
        helpString: 'List attachments in the Data Bank as a JSON-serialized array. Optionally, provide the source of the attachments and the field to list by.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source of the attachments.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
 | 
						|
            new SlashCommandNamedArgument('field', 'The field to list by.', ARGUMENT_TYPE.STRING, false, false, 'url', FIELDS),
 | 
						|
        ],
 | 
						|
        returns: ARGUMENT_TYPE.LIST,
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-get',
 | 
						|
        callback: getDataBankText,
 | 
						|
        aliases: ['databank-get', 'data-bank-get'],
 | 
						|
        helpString: 'Get attachment text from the Data Bank. Either provide the name or URL of the attachment. Optionally, provide the source of the attachment.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'The name or URL of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                isRequired: true,
 | 
						|
                acceptsMultiple: false,
 | 
						|
                enumProvider: localEnumProviders.attachments('name', ''),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
        returns: ARGUMENT_TYPE.STRING,
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-add',
 | 
						|
        callback: uploadDataBankAttachment,
 | 
						|
        aliases: ['databank-add', 'data-bank-add'],
 | 
						|
        helpString: 'Add an attachment to the Data Bank. If name is not provided, it will be generated automatically. Returns the URL of the attachment.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
 | 
						|
            new SlashCommandNamedArgument('name', 'The name of the attachment.', ARGUMENT_TYPE.STRING, false, false),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
 | 
						|
        ],
 | 
						|
        returns: ARGUMENT_TYPE.STRING,
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-update',
 | 
						|
        callback: updateDataBankAttachment,
 | 
						|
        aliases: ['databank-update', 'data-bank-update'],
 | 
						|
        helpString: 'Update an attachment in the Data Bank, preserving its name. Returns a new URL of the attachment.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source for the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
 | 
						|
            SlashCommandNamedArgument.fromProps({
 | 
						|
                name: 'name',
 | 
						|
                description: 'The name of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                enumProvider: localEnumProviders.attachments('name'),
 | 
						|
            }),
 | 
						|
            SlashCommandNamedArgument.fromProps({
 | 
						|
                name: 'url',
 | 
						|
                description: 'The URL of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                enumProvider: localEnumProviders.attachments('url'),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            new SlashCommandArgument('The content of the file attachment.', ARGUMENT_TYPE.STRING, true, false),
 | 
						|
        ],
 | 
						|
        returns: ARGUMENT_TYPE.STRING,
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-disable',
 | 
						|
        callback: disableDataBankAttachment,
 | 
						|
        aliases: ['databank-disable', 'data-bank-disable'],
 | 
						|
        helpString: 'Disable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'The name or URL of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                isRequired: true,
 | 
						|
                enumProvider: localEnumProviders.attachments('name', ''),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-enable',
 | 
						|
        callback: enableDataBankAttachment,
 | 
						|
        aliases: ['databank-enable', 'data-bank-enable'],
 | 
						|
        helpString: 'Enable an attachment in the Data Bank by its name or URL. Optionally, provide the source of the attachment.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, '', TYPES),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'The name or URL of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                isRequired: true,
 | 
						|
                enumProvider: localEnumProviders.attachments('name', ''),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
    }));
 | 
						|
 | 
						|
    SlashCommandParser.addCommandObject(SlashCommand.fromProps({
 | 
						|
        name: 'db-delete',
 | 
						|
        callback: deleteDataBankAttachment,
 | 
						|
        aliases: ['databank-delete', 'data-bank-delete'],
 | 
						|
        helpString: 'Delete an attachment from the Data Bank.',
 | 
						|
        namedArgumentList: [
 | 
						|
            new SlashCommandNamedArgument('source', 'The source of the attachment.', ARGUMENT_TYPE.STRING, false, false, 'chat', TYPES),
 | 
						|
        ],
 | 
						|
        unnamedArgumentList: [
 | 
						|
            SlashCommandArgument.fromProps({
 | 
						|
                description: 'The name or URL of the attachment.',
 | 
						|
                typeList: [ARGUMENT_TYPE.STRING],
 | 
						|
                isRequired: true,
 | 
						|
                enumProvider: localEnumProviders.attachments(),
 | 
						|
            }),
 | 
						|
        ],
 | 
						|
    }));
 | 
						|
});
 |