mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-03 03:17:54 +01:00
Add Data Bank manager
This commit is contained in:
parent
71041ec764
commit
242d57c14b
@ -491,6 +491,10 @@ textarea:disabled {
|
|||||||
font-size: calc(var(--mainFontSize) * 1.2) !important;
|
font-size: calc(var(--mainFontSize) * 1.2) !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.fontsize90p {
|
||||||
|
font-size: calc(var(--mainFontSize) * 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
.fontsize80p {
|
.fontsize80p {
|
||||||
font-size: calc(var(--mainFontSize) * 0.8) !important;
|
font-size: calc(var(--mainFontSize) * 0.8) !important;
|
||||||
}
|
}
|
||||||
|
@ -7361,47 +7361,6 @@ export function cancelTtsPlay() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessageImage() {
|
|
||||||
const value = await callPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', 'confirm');
|
|
||||||
|
|
||||||
if (!value) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const mesBlock = $(this).closest('.mes');
|
|
||||||
const mesId = mesBlock.attr('mesid');
|
|
||||||
const message = chat[mesId];
|
|
||||||
delete message.extra.image;
|
|
||||||
delete message.extra.inline_image;
|
|
||||||
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
|
||||||
mesBlock.find('.mes_img').attr('src', '');
|
|
||||||
await saveChatConditional();
|
|
||||||
}
|
|
||||||
|
|
||||||
function enlargeMessageImage() {
|
|
||||||
const mesBlock = $(this).closest('.mes');
|
|
||||||
const mesId = mesBlock.attr('mesid');
|
|
||||||
const message = chat[mesId];
|
|
||||||
const imgSrc = message?.extra?.image;
|
|
||||||
const title = message?.extra?.title;
|
|
||||||
|
|
||||||
if (!imgSrc) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const img = document.createElement('img');
|
|
||||||
img.classList.add('img_enlarged');
|
|
||||||
img.src = imgSrc;
|
|
||||||
const imgContainer = $('<div><pre><code></code></pre></div>');
|
|
||||||
imgContainer.prepend(img);
|
|
||||||
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);
|
|
||||||
callPopup(imgContainer, 'text', '', { wide: true, large: true });
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateAlternateGreetingsHintVisibility(root) {
|
function updateAlternateGreetingsHintVisibility(root) {
|
||||||
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
|
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
|
||||||
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
|
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
|
||||||
@ -10392,9 +10351,6 @@ jQuery(async function () {
|
|||||||
$('#char-management-dropdown').prop('selectedIndex', 0);
|
$('#char-management-dropdown').prop('selectedIndex', 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
|
||||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
|
||||||
|
|
||||||
$(window).on('beforeunload', () => {
|
$(window).on('beforeunload', () => {
|
||||||
cancelTtsPlay();
|
cancelTtsPlay();
|
||||||
if (streamingProcessor) {
|
if (streamingProcessor) {
|
||||||
|
@ -18,6 +18,8 @@ import {
|
|||||||
saveSettingsDebounced,
|
saveSettingsDebounced,
|
||||||
showSwipeButtons,
|
showSwipeButtons,
|
||||||
this_chid,
|
this_chid,
|
||||||
|
saveChatConditional,
|
||||||
|
chat_metadata,
|
||||||
} from '../script.js';
|
} from '../script.js';
|
||||||
import { selected_group } from './group-chats.js';
|
import { selected_group } from './group-chats.js';
|
||||||
import { power_user } from './power-user.js';
|
import { power_user } from './power-user.js';
|
||||||
@ -29,9 +31,25 @@ import {
|
|||||||
getStringHash,
|
getStringHash,
|
||||||
humanFileSize,
|
humanFileSize,
|
||||||
saveBase64AsFile,
|
saveBase64AsFile,
|
||||||
|
isValidUrl,
|
||||||
} from './utils.js';
|
} from './utils.js';
|
||||||
|
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced, writeExtensionField } from './extensions.js';
|
||||||
|
import { POPUP_RESULT, POPUP_TYPE, callGenericPopup } from './popup.js';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @typedef {Object} FileAttachment
|
||||||
|
* @property {string} url File URL
|
||||||
|
* @property {number} size File size
|
||||||
|
* @property {string} name File name
|
||||||
|
* @property {string} [text] File text
|
||||||
|
*/
|
||||||
|
|
||||||
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
const fileSizeLimit = 1024 * 1024 * 10; // 10 MB
|
||||||
|
const ATTACHMENT_SOURCE = {
|
||||||
|
GLOBAL: 'global',
|
||||||
|
CHAT: 'chat',
|
||||||
|
CHARACTER: 'character',
|
||||||
|
};
|
||||||
|
|
||||||
const converters = {
|
const converters = {
|
||||||
'application/pdf': extractTextFromPDF,
|
'application/pdf': extractTextFromPDF,
|
||||||
@ -39,6 +57,11 @@ const converters = {
|
|||||||
'text/markdown': extractTextFromMarkdown,
|
'text/markdown': extractTextFromMarkdown,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if the file type has a converter function.
|
||||||
|
* @param {string} type MIME type
|
||||||
|
* @returns {boolean} True if the file type is convertible, false otherwise.
|
||||||
|
*/
|
||||||
function isConvertible(type) {
|
function isConvertible(type) {
|
||||||
return Object.keys(converters).includes(type);
|
return Object.keys(converters).includes(type);
|
||||||
}
|
}
|
||||||
@ -275,9 +298,9 @@ async function onFileAttach() {
|
|||||||
* @param {number} messageId Message ID
|
* @param {number} messageId Message ID
|
||||||
*/
|
*/
|
||||||
async function deleteMessageFile(messageId) {
|
async function deleteMessageFile(messageId) {
|
||||||
const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm');
|
const confirm = await callGenericPopup('Are you sure you want to delete this file?', POPUP_TYPE.CONFIRM);
|
||||||
|
|
||||||
if (!confirm) {
|
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||||
console.debug('Delete file cancelled');
|
console.debug('Delete file cancelled');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -289,11 +312,15 @@ async function deleteMessageFile(messageId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const url = message.extra.file.url;
|
||||||
|
|
||||||
delete message.extra.file;
|
delete message.extra.file;
|
||||||
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
||||||
saveChatDebounced();
|
await saveChatConditional();
|
||||||
|
await deleteFileFromServer(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Opens file from message in a modal.
|
* Opens file from message in a modal.
|
||||||
* @param {number} messageId Message ID
|
* @param {number} messageId Message ID
|
||||||
@ -306,14 +333,7 @@ async function viewMessageFile(messageId) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fileText = messageFile.text || (await getFileAttachment(messageFile.url));
|
await openFilePopup(messageFile);
|
||||||
|
|
||||||
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
|
||||||
modalTemplate.find('code').addClass('txt').text(fileText);
|
|
||||||
modalTemplate.addClass('file_modal');
|
|
||||||
addCopyToCodeBlocks(modalTemplate);
|
|
||||||
|
|
||||||
callPopup(modalTemplate, 'text', '', { wide: true, large: true });
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -348,7 +368,7 @@ function embedMessageFile(messageId, messageBlock) {
|
|||||||
|
|
||||||
await populateFileAttachment(message, 'embed_file_input');
|
await populateFileAttachment(message, 'embed_file_input');
|
||||||
appendMediaToMessage(message, messageBlock);
|
appendMediaToMessage(message, messageBlock);
|
||||||
saveChatDebounced();
|
await saveChatConditional();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -476,6 +496,366 @@ export function isExternalMediaAllowed() {
|
|||||||
return !power_user.forbid_external_images;
|
return !power_user.forbid_external_images;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function enlargeMessageImage() {
|
||||||
|
const mesBlock = $(this).closest('.mes');
|
||||||
|
const mesId = mesBlock.attr('mesid');
|
||||||
|
const message = chat[mesId];
|
||||||
|
const imgSrc = message?.extra?.image;
|
||||||
|
const title = message?.extra?.title;
|
||||||
|
|
||||||
|
if (!imgSrc) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const img = document.createElement('img');
|
||||||
|
img.classList.add('img_enlarged');
|
||||||
|
img.src = imgSrc;
|
||||||
|
const imgContainer = $('<div><pre><code></code></pre></div>');
|
||||||
|
imgContainer.prepend(img);
|
||||||
|
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 });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function deleteMessageImage() {
|
||||||
|
const value = await callGenericPopup('<h3>Delete image from message?<br>This action can\'t be undone.</h3>', POPUP_TYPE.CONFIRM);
|
||||||
|
|
||||||
|
if (value !== POPUP_RESULT.AFFIRMATIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const mesBlock = $(this).closest('.mes');
|
||||||
|
const mesId = mesBlock.attr('mesid');
|
||||||
|
const message = chat[mesId];
|
||||||
|
delete message.extra.image;
|
||||||
|
delete message.extra.inline_image;
|
||||||
|
mesBlock.find('.mes_img_container').removeClass('img_extra');
|
||||||
|
mesBlock.find('.mes_img').attr('src', '');
|
||||||
|
await saveChatConditional();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes file from the server.
|
||||||
|
* @param {string} url Path to the file on the server
|
||||||
|
* @returns {Promise<boolean>} True if file was deleted, false otherwise.
|
||||||
|
*/
|
||||||
|
async function deleteFileFromServer(url) {
|
||||||
|
try {
|
||||||
|
const result = await fetch('/api/files/delete', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ path: url }),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!result.ok) {
|
||||||
|
const error = await result.text();
|
||||||
|
throw new Error(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error(String(error), 'Could not delete file');
|
||||||
|
console.error('Could not delete file', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens file attachment in a modal.
|
||||||
|
* @param {FileAttachment} attachment File attachment
|
||||||
|
*/
|
||||||
|
async function openFilePopup(attachment) {
|
||||||
|
const fileText = attachment.text || (await getFileAttachment(attachment.url));
|
||||||
|
|
||||||
|
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||||
|
modalTemplate.find('code').addClass('txt').text(fileText);
|
||||||
|
modalTemplate.addClass('file_modal').addClass('textarea_compact').addClass('fontsize90p');
|
||||||
|
addCopyToCodeBlocks(modalTemplate);
|
||||||
|
|
||||||
|
callGenericPopup(modalTemplate, POPUP_TYPE.TEXT, '', { wide: true, large: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes an attachment from the server and the chat.
|
||||||
|
* @param {FileAttachment} attachment Attachment to delete
|
||||||
|
* @param {string} source Source of the attachment
|
||||||
|
* @param {function} callback Callback function
|
||||||
|
* @returns {Promise<void>} A promise that resolves when the attachment is deleted.
|
||||||
|
*/
|
||||||
|
async function deleteAttachment(attachment, source, callback) {
|
||||||
|
const confirm = await callGenericPopup('Are you sure you want to delete this attachment?', POPUP_TYPE.CONFIRM);
|
||||||
|
|
||||||
|
if (confirm !== POPUP_RESULT.AFFIRMATIVE) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ensureAttachmentsExist();
|
||||||
|
|
||||||
|
switch (source) {
|
||||||
|
case 'global':
|
||||||
|
extension_settings.attachments = extension_settings.attachments.filter((a) => a.url !== attachment.url);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
break;
|
||||||
|
case 'chat':
|
||||||
|
chat_metadata.attachments = chat_metadata.attachments.filter((a) => a.url !== attachment.url);
|
||||||
|
saveMetadataDebounced();
|
||||||
|
break;
|
||||||
|
case 'character':
|
||||||
|
characters[this_chid].data.extensions.attachments = characters[this_chid].data.extensions.attachments.filter((a) => a.url !== attachment.url);
|
||||||
|
await writeExtensionField(this_chid, 'attachments', characters[this_chid].data.extensions.attachments);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
await deleteFileFromServer(attachment.url);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens the attachment manager.
|
||||||
|
*/
|
||||||
|
async function openAttachmentManager() {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {FileAttachment[]} attachments List of attachments
|
||||||
|
* @param {string} source Source of the attachments
|
||||||
|
*/
|
||||||
|
async function renderList(attachments, source) {
|
||||||
|
const sources = {
|
||||||
|
[ATTACHMENT_SOURCE.GLOBAL]: '.globalAttachmentsList',
|
||||||
|
[ATTACHMENT_SOURCE.CHARACTER]: '.characterAttachmentsList',
|
||||||
|
[ATTACHMENT_SOURCE.CHAT]: '.chatAttachmentsList',
|
||||||
|
};
|
||||||
|
|
||||||
|
template.find(sources[source]).empty();
|
||||||
|
for (const attachment of attachments) {
|
||||||
|
const attachmentTemplate = template.find('.attachmentListItemTemplate .attachmentListItem').clone();
|
||||||
|
attachmentTemplate.find('.attachmentListItemName').text(attachment.name);
|
||||||
|
attachmentTemplate.find('.attachmentListItemSize').text(humanFileSize(attachment.size));
|
||||||
|
attachmentTemplate.find('.viewAttachmentButton').on('click', () => openFilePopup(attachment));
|
||||||
|
attachmentTemplate.find('.deleteAttachmentButton').on('click', () => deleteAttachment(attachment, source, renderAttachments));
|
||||||
|
template.find(sources[source]).append(attachmentTemplate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function renderAttachments() {
|
||||||
|
/** @type {FileAttachment[]} */
|
||||||
|
const globalAttachments = extension_settings.attachments ?? [];
|
||||||
|
/** @type {FileAttachment[]} */
|
||||||
|
const chatAttachments = chat_metadata.attachments ?? [];
|
||||||
|
/** @type {FileAttachment[]} */
|
||||||
|
const characterAttachments = characters[this_chid]?.data?.extensions?.attachments ?? [];
|
||||||
|
|
||||||
|
await renderList(globalAttachments, ATTACHMENT_SOURCE.GLOBAL);
|
||||||
|
await renderList(chatAttachments, ATTACHMENT_SOURCE.CHAT);
|
||||||
|
await renderList(characterAttachments, ATTACHMENT_SOURCE.CHARACTER);
|
||||||
|
|
||||||
|
const isNotCharacter = this_chid === undefined || selected_group;
|
||||||
|
const isNotInChat = getCurrentChatId() === undefined;
|
||||||
|
template.find('.characterAttachmentsBlock').toggle(!isNotCharacter);
|
||||||
|
template.find('.chatAttachmentsBlock').toggle(!isNotInChat);
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasFandomPlugin = await isFandomPluginAvailable();
|
||||||
|
const template = $(await renderExtensionTemplateAsync('attachments', 'manager', {}));
|
||||||
|
template.find('.scrapeWebpageButton').on('click', function () {
|
||||||
|
openWebpageScraper(String($(this).data('attachment-manager-target')), renderAttachments);
|
||||||
|
});
|
||||||
|
template.find('.scrapeFandomButton').toggle(hasFandomPlugin).on('click', function () {
|
||||||
|
openFandomScraper(String($(this).data('attachment-manager-target')), renderAttachments);
|
||||||
|
});
|
||||||
|
template.find('.uploadFileButton').on('click', function () {
|
||||||
|
openFileUploader(String($(this).data('attachment-manager-target')), renderAttachments);
|
||||||
|
});
|
||||||
|
await renderAttachments();
|
||||||
|
callGenericPopup(template, POPUP_TYPE.TEXT, '', { wide: true, large: true, okButton: 'Close' });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrapes a webpage for attachments.
|
||||||
|
* @param {string} target Target for the attachment
|
||||||
|
* @param {function} callback Callback function
|
||||||
|
*/
|
||||||
|
async function openWebpageScraper(target, callback) {
|
||||||
|
const template = $(await renderExtensionTemplateAsync('attachments', 'web-scrape', {}));
|
||||||
|
const link = await callGenericPopup(template, POPUP_TYPE.INPUT, '', { wide: false, large: false });
|
||||||
|
|
||||||
|
if (!link) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
if (!isValidUrl(link)) {
|
||||||
|
toastr.error('Invalid URL');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await fetch('/api/serpapi/visit', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
body: JSON.stringify({ url: link }),
|
||||||
|
});
|
||||||
|
|
||||||
|
const blob = await result.blob();
|
||||||
|
const domain = new URL(link).hostname;
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const title = await getTitleFromHtmlBlob(blob) || 'webpage';
|
||||||
|
const file = new File([blob], `${title} - ${domain} - ${timestamp}.html`, { type: 'text/html' });
|
||||||
|
await uploadFileAttachmentToServer(file, target);
|
||||||
|
callback();
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Scraping failed', error);
|
||||||
|
toastr.error('Check browser console for details.', 'Scraping failed');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {Blob} blob Blob of the HTML file
|
||||||
|
* @returns {Promise<string>} Title of the HTML file
|
||||||
|
*/
|
||||||
|
async function getTitleFromHtmlBlob(blob) {
|
||||||
|
const text = await blob.text();
|
||||||
|
const titleMatch = text.match(/<title>(.*?)<\/title>/i);
|
||||||
|
return titleMatch ? titleMatch[1] : '';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scrapes a Fandom page for attachments.
|
||||||
|
* @param {string} target Target for the attachment
|
||||||
|
* @param {function} callback Callback function
|
||||||
|
*/
|
||||||
|
async function openFandomScraper(target, callback) {
|
||||||
|
toastr.info('Not implemented yet', target);
|
||||||
|
callback();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file attachment.
|
||||||
|
* @param {string} target File upload target
|
||||||
|
* @param {function} callback Callback function
|
||||||
|
*/
|
||||||
|
async function openFileUploader(target, callback) {
|
||||||
|
const fileInput = document.createElement('input');
|
||||||
|
fileInput.type = 'file';
|
||||||
|
fileInput.accept = '.txt, .md, .pdf, .html, .htm';
|
||||||
|
fileInput.onchange = async function () {
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
await uploadFileAttachmentToServer(file, target);
|
||||||
|
|
||||||
|
callback();
|
||||||
|
};
|
||||||
|
|
||||||
|
fileInput.click();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Uploads a file attachment to the server.
|
||||||
|
* @param {File} file File to upload
|
||||||
|
* @param {string} target Target for the attachment
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function uploadFileAttachmentToServer(file, target) {
|
||||||
|
const isValid = await validateFile(file);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let base64Data = await getBase64Async(file);
|
||||||
|
const slug = getStringHash(file.name);
|
||||||
|
const uniqueFileName = `${Date.now()}_${slug}.txt`;
|
||||||
|
|
||||||
|
if (isConvertible(file.type)) {
|
||||||
|
try {
|
||||||
|
const converter = converters[file.type];
|
||||||
|
const fileText = await converter(file);
|
||||||
|
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||||
|
} catch (error) {
|
||||||
|
toastr.error(String(error), 'Could not convert file');
|
||||||
|
console.error('Could not convert file', error);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
const fileText = await file.text();
|
||||||
|
base64Data = window.btoa(unescape(encodeURIComponent(fileText)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const fileUrl = await uploadFileAttachment(uniqueFileName, base64Data);
|
||||||
|
|
||||||
|
if (!fileUrl) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attachment = {
|
||||||
|
url: fileUrl,
|
||||||
|
size: file.size,
|
||||||
|
name: file.name,
|
||||||
|
};
|
||||||
|
|
||||||
|
ensureAttachmentsExist();
|
||||||
|
|
||||||
|
switch (target) {
|
||||||
|
case ATTACHMENT_SOURCE.GLOBAL:
|
||||||
|
extension_settings.attachments.push(attachment);
|
||||||
|
saveSettingsDebounced();
|
||||||
|
break;
|
||||||
|
case ATTACHMENT_SOURCE.CHAT:
|
||||||
|
chat_metadata.attachments.push(attachment);
|
||||||
|
saveMetadataDebounced();
|
||||||
|
break;
|
||||||
|
case ATTACHMENT_SOURCE.CHARACTER:
|
||||||
|
characters[this_chid].data.extensions.attachments.push(attachment);
|
||||||
|
await writeExtensionField(this_chid, 'attachments', characters[this_chid].data.extensions.attachments);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function ensureAttachmentsExist() {
|
||||||
|
if (!Array.isArray(extension_settings.attachments)) {
|
||||||
|
extension_settings.attachments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(chat_metadata.attachments)) {
|
||||||
|
chat_metadata.attachments = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this_chid !== undefined && characters[this_chid]) {
|
||||||
|
if (!characters[this_chid].data) {
|
||||||
|
characters[this_chid].data = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!characters[this_chid].data.extensions) {
|
||||||
|
characters[this_chid].data.extensions = {};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(characters[this_chid]?.data?.extensions?.attachments)) {
|
||||||
|
characters[this_chid].data.extensions.attachments = [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Probes the server to check if the Fandom plugin is available.
|
||||||
|
* @returns {Promise<boolean>} True if the plugin is available, false otherwise.
|
||||||
|
*/
|
||||||
|
async function isFandomPluginAvailable() {
|
||||||
|
try {
|
||||||
|
const result = await fetch('/api/plugins/fandom/probe', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: getRequestHeaders(),
|
||||||
|
});
|
||||||
|
|
||||||
|
return result.ok;
|
||||||
|
} catch (error) {
|
||||||
|
console.debug('Could not probe Fandom plugin', error);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
jQuery(function () {
|
jQuery(function () {
|
||||||
$(document).on('click', '.mes_hide', async function () {
|
$(document).on('click', '.mes_hide', async function () {
|
||||||
const messageBlock = $(this).closest('.mes');
|
const messageBlock = $(this).closest('.mes');
|
||||||
@ -506,6 +886,11 @@ jQuery(function () {
|
|||||||
$('#file_form_input').trigger('click');
|
$('#file_form_input').trigger('click');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Do not change. #manageAttachments is added by extension.
|
||||||
|
$(document).on('click', '#manageAttachments', function () {
|
||||||
|
openAttachmentManager();
|
||||||
|
});
|
||||||
|
|
||||||
$(document).on('click', '.mes_embed', function () {
|
$(document).on('click', '.mes_embed', function () {
|
||||||
const messageBlock = $(this).closest('.mes');
|
const messageBlock = $(this).closest('.mes');
|
||||||
const messageId = Number(messageBlock.attr('mesid'));
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
@ -597,6 +982,9 @@ jQuery(function () {
|
|||||||
reloadCurrentChat();
|
reloadCurrentChat();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||||
|
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||||
|
|
||||||
$('#file_form_input').on('change', onFileAttach);
|
$('#file_form_input').on('change', onFileAttach);
|
||||||
$('#file_form').on('reset', function () {
|
$('#file_form').on('reset', function () {
|
||||||
$('#file_form').addClass('displayNone');
|
$('#file_form').addClass('displayNone');
|
||||||
|
@ -145,6 +145,7 @@ const extension_settings = {
|
|||||||
variables: {
|
variables: {
|
||||||
global: {},
|
global: {},
|
||||||
},
|
},
|
||||||
|
attachments: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
let modules = [];
|
let modules = [];
|
||||||
|
9
public/scripts/extensions/attachments/buttons.html
Normal file
9
public/scripts/extensions/attachments/buttons.html
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
<div id="attachFile" class="list-group-item flex-container flexGap5" title="Attach a file or image to a current chat.">
|
||||||
|
<div class="fa-fw fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
|
||||||
|
<span data-i18n="Attach a File">Attach a File</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="manageAttachments" class="list-group-item flex-container flexGap5" title="View global, character, or data files.">
|
||||||
|
<div class="fa-fw fa-solid fa-book-open-reader extensionsMenuExtensionButton"></div>
|
||||||
|
<span data-i18n="Open Data Bank">Open Data Bank</span>
|
||||||
|
</div>
|
6
public/scripts/extensions/attachments/index.js
Normal file
6
public/scripts/extensions/attachments/index.js
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
import { renderExtensionTemplateAsync } from '../../extensions.js';
|
||||||
|
|
||||||
|
jQuery(async () => {
|
||||||
|
const buttons = await renderExtensionTemplateAsync('attachments', 'buttons', {});
|
||||||
|
$('#extensionsMenu').prepend(buttons);
|
||||||
|
});
|
116
public/scripts/extensions/attachments/manager.html
Normal file
116
public/scripts/extensions/attachments/manager.html
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
<div class="wide100p paddingTopBot5">
|
||||||
|
<h2 class="marginBot5">
|
||||||
|
<span data-i18n="Data Bank">
|
||||||
|
Data Bank
|
||||||
|
</span>
|
||||||
|
</h2>
|
||||||
|
<div data-i18n="These files will be available for extensions that support attachments (e.g. Vector Storage).">
|
||||||
|
These files will be available for extensions that support attachments (e.g. Vector Storage).
|
||||||
|
</div>
|
||||||
|
<div data-i18n="Supported file types: Plain Text, PDF, Markdown, HTML." class="marginTopBot5">
|
||||||
|
Supported file types: Plain Text, PDF, Markdown, HTML.
|
||||||
|
</div>
|
||||||
|
<div class="justifyLeft globalAttachmentsBlock marginBot10">
|
||||||
|
<h3 class="margin0 title_restorable">
|
||||||
|
<span data-i18n="Global Attachments">
|
||||||
|
Global Attachments
|
||||||
|
</span>
|
||||||
|
<div class="flex-container flexGap10">
|
||||||
|
<div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Download a page from the web.">
|
||||||
|
<i class="fa-fw fa-solid fa-globe"></i>
|
||||||
|
<span data-i18n="From Web">
|
||||||
|
From Web
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Download a page from the Fandom wiki.">
|
||||||
|
<i class="fa-fw fa-solid fa-fan"></i>
|
||||||
|
<span data-i18n="From Fandom">
|
||||||
|
From Fandom
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="global" title="Upload a file from your computer.">
|
||||||
|
<i class="fa-fw fa-solid fa-upload"></i>
|
||||||
|
<span data-i18n="From File">
|
||||||
|
From File
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<small data-i18n="These files are available for all characters in all chats.">
|
||||||
|
These files are available for all characters in all chats.
|
||||||
|
</small>
|
||||||
|
<div class="globalAttachmentsList attachmentsList"></div>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="justifyLeft characterAttachmentsBlock marginBot10">
|
||||||
|
<h3 class="margin0 title_restorable">
|
||||||
|
<span data-i18n="Character Attachments">Character Attachments</span>
|
||||||
|
<div class="flex-container flexGap10">
|
||||||
|
<div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Download a page from the web.">
|
||||||
|
<i class="fa-fw fa-solid fa-globe"></i>
|
||||||
|
<span data-i18n="From Web">
|
||||||
|
From Web
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Download a page from the Fandom wiki.">
|
||||||
|
<i class="fa-fw fa-solid fa-fan"></i>
|
||||||
|
<span data-i18n="From Fandom">
|
||||||
|
From Fandom
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="character" title="Upload a file from your computer.">
|
||||||
|
<i class="fa-fw fa-solid fa-upload"></i>
|
||||||
|
<span data-i18n="From File">
|
||||||
|
From File
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<small data-i18n="These files are available the current character in all chats they are in.">
|
||||||
|
These files are available the current character in all chats they are in.
|
||||||
|
</small>
|
||||||
|
<div class="characterAttachmentsList attachmentsList"></div>
|
||||||
|
<hr>
|
||||||
|
</div>
|
||||||
|
<div class="justifyLeft chatAttachmentsBlock marginBot10">
|
||||||
|
<h3 class="margin0 title_restorable">
|
||||||
|
<span data-i18n="Chat Attachments">
|
||||||
|
Chat Attachments
|
||||||
|
</span>
|
||||||
|
<div class="flex-container flexGap10">
|
||||||
|
<div class="scrapeWebpageButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Download a page from the web.">
|
||||||
|
<i class="fa-fw fa-solid fa-globe"></i>
|
||||||
|
<span data-i18n="From Web">
|
||||||
|
From Web
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="scrapeFandomButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Download a page from the Fandom wiki.">
|
||||||
|
<i class="fa-fw fa-solid fa-fan"></i>
|
||||||
|
<span data-i18n="From Fandom">
|
||||||
|
From Fandom
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="uploadFileButton menu_button_icon menu_button" data-attachment-manager-target="chat" title="Upload a file from your computer.">
|
||||||
|
<i class="fa-fw fa-solid fa-upload"></i>
|
||||||
|
<span data-i18n="From File">
|
||||||
|
From File
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</h3>
|
||||||
|
<small data-i18n="These files are available to all characters in the current chat.">
|
||||||
|
These files are available to all characters in the current chat.
|
||||||
|
</small>
|
||||||
|
<div class="chatAttachmentsList attachmentsList"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="attachmentListItemTemplate template_element">
|
||||||
|
<div class="attachmentListItem flex-container alignItemsCenter flexGap10">
|
||||||
|
<div class="attachmentFileIcon fa-solid fa-file-alt"></div>
|
||||||
|
<div class="attachmentListItemName flex1"></div>
|
||||||
|
<small class="attachmentListItemSize"></small>
|
||||||
|
<div class="viewAttachmentButton right_menu_button fa-solid fa-magnifying-glass" title="View attachment content"></div>
|
||||||
|
<div class="deleteAttachmentButton right_menu_button fa-solid fa-trash" title="Delete attachment"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
11
public/scripts/extensions/attachments/manifest.json
Normal file
11
public/scripts/extensions/attachments/manifest.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"display_name": "Chat Attachments",
|
||||||
|
"loading_order": 3,
|
||||||
|
"requires": [],
|
||||||
|
"optional": [],
|
||||||
|
"js": "index.js",
|
||||||
|
"css": "style.css",
|
||||||
|
"author": "Cohee1207",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"homePage": "https://github.com/SillyTavern/SillyTavern"
|
||||||
|
}
|
20
public/scripts/extensions/attachments/style.css
Normal file
20
public/scripts/extensions/attachments/style.css
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
.attachmentsList:empty {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachmentsList:empty::before {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
content: "No data";
|
||||||
|
font-weight: bolder;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
opacity: 0.8;
|
||||||
|
min-height: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.attachmentListItem {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
3
public/scripts/extensions/attachments/web-scrape.html
Normal file
3
public/scripts/extensions/attachments/web-scrape.html
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
<div data-i18n="Enter a web address to scrape:">
|
||||||
|
Enter a web address to scrape:
|
||||||
|
</div>
|
@ -270,14 +270,8 @@ jQuery(function () {
|
|||||||
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
||||||
Generate Caption
|
Generate Caption
|
||||||
</div>`);
|
</div>`);
|
||||||
const attachFileButton = $(`
|
|
||||||
<div id="attachFile" class="list-group-item flex-container flexGap5">
|
|
||||||
<div class="fa-solid fa-paperclip extensionsMenuExtensionButton"></div>
|
|
||||||
Attach a File
|
|
||||||
</div>`);
|
|
||||||
|
|
||||||
$('#extensionsMenu').prepend(sendButton);
|
$('#extensionsMenu').prepend(sendButton);
|
||||||
$('#extensionsMenu').prepend(attachFileButton);
|
|
||||||
$(sendButton).on('click', () => {
|
$(sendButton).on('click', () => {
|
||||||
const hasCaptionModule =
|
const hasCaptionModule =
|
||||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||||
|
@ -1,4 +1,5 @@
|
|||||||
const path = require('path');
|
const path = require('path');
|
||||||
|
const fs = require('fs');
|
||||||
const writeFileSyncAtomic = require('write-file-atomic').sync;
|
const writeFileSyncAtomic = require('write-file-atomic').sync;
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
@ -24,6 +25,7 @@ router.post('/upload', jsonParser, async (request, response) => {
|
|||||||
const pathToUpload = path.join(request.user.directories.files, request.body.name);
|
const pathToUpload = path.join(request.user.directories.files, request.body.name);
|
||||||
writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
|
writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
|
||||||
const url = clientRelativePath(request.user.directories.root, pathToUpload);
|
const url = clientRelativePath(request.user.directories.root, pathToUpload);
|
||||||
|
console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);
|
||||||
return response.send({ path: url });
|
return response.send({ path: url });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
@ -31,4 +33,28 @@ router.post('/upload', jsonParser, async (request, response) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
router.post('/delete', jsonParser, async (request, response) => {
|
||||||
|
try {
|
||||||
|
if (!request.body.path) {
|
||||||
|
return response.status(400).send('No path specified');
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathToDelete = path.join(request.user.directories.root, request.body.path);
|
||||||
|
if (!pathToDelete.startsWith(request.user.directories.files)) {
|
||||||
|
return response.status(400).send('Invalid path');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(pathToDelete)) {
|
||||||
|
return response.status(404).send('File not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.rmSync(pathToDelete);
|
||||||
|
console.log(`Deleted file: ${request.body.path} from ${request.user.profile.handle}`);
|
||||||
|
return response.sendStatus(200);
|
||||||
|
} catch (error) {
|
||||||
|
console.log(error);
|
||||||
|
return response.sendStatus(500);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
module.exports = { router };
|
module.exports = { router };
|
||||||
|
@ -5,10 +5,10 @@ const { jsonParser } = require('../express-common');
|
|||||||
|
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
|
|
||||||
// Cosplay as Firefox
|
// Cosplay as Chrome
|
||||||
const visitHeaders = {
|
const visitHeaders = {
|
||||||
'Accept': 'text/html',
|
'Accept': 'text/html',
|
||||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:120.0) Gecko/20100101 Firefox/120.0',
|
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36',
|
||||||
'Accept-Language': 'en-US,en;q=0.5',
|
'Accept-Language': 'en-US,en;q=0.5',
|
||||||
'Accept-Encoding': 'gzip, deflate, br',
|
'Accept-Encoding': 'gzip, deflate, br',
|
||||||
'Connection': 'keep-alive',
|
'Connection': 'keep-alive',
|
||||||
|
Loading…
x
Reference in New Issue
Block a user