mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-03-02 02:47:52 +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;
|
||||
}
|
||||
|
||||
.fontsize90p {
|
||||
font-size: calc(var(--mainFontSize) * 0.9) !important;
|
||||
}
|
||||
|
||||
.fontsize80p {
|
||||
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) {
|
||||
const numberOfGreetings = root.find('.alternate_greetings_list .alternate_greeting').length;
|
||||
$(root).find('.alternate_grettings_hint').toggle(numberOfGreetings == 0);
|
||||
@ -10392,9 +10351,6 @@ jQuery(async function () {
|
||||
$('#char-management-dropdown').prop('selectedIndex', 0);
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$(window).on('beforeunload', () => {
|
||||
cancelTtsPlay();
|
||||
if (streamingProcessor) {
|
||||
|
@ -18,6 +18,8 @@ import {
|
||||
saveSettingsDebounced,
|
||||
showSwipeButtons,
|
||||
this_chid,
|
||||
saveChatConditional,
|
||||
chat_metadata,
|
||||
} from '../script.js';
|
||||
import { selected_group } from './group-chats.js';
|
||||
import { power_user } from './power-user.js';
|
||||
@ -29,9 +31,25 @@ import {
|
||||
getStringHash,
|
||||
humanFileSize,
|
||||
saveBase64AsFile,
|
||||
isValidUrl,
|
||||
} 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 ATTACHMENT_SOURCE = {
|
||||
GLOBAL: 'global',
|
||||
CHAT: 'chat',
|
||||
CHARACTER: 'character',
|
||||
};
|
||||
|
||||
const converters = {
|
||||
'application/pdf': extractTextFromPDF,
|
||||
@ -39,6 +57,11 @@ const converters = {
|
||||
'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) {
|
||||
return Object.keys(converters).includes(type);
|
||||
}
|
||||
@ -275,9 +298,9 @@ async function onFileAttach() {
|
||||
* @param {number} messageId Message ID
|
||||
*/
|
||||
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');
|
||||
return;
|
||||
}
|
||||
@ -289,11 +312,15 @@ async function deleteMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = message.extra.file.url;
|
||||
|
||||
delete message.extra.file;
|
||||
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
await deleteFileFromServer(url);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Opens file from message in a modal.
|
||||
* @param {number} messageId Message ID
|
||||
@ -306,14 +333,7 @@ async function viewMessageFile(messageId) {
|
||||
return;
|
||||
}
|
||||
|
||||
const fileText = messageFile.text || (await getFileAttachment(messageFile.url));
|
||||
|
||||
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 });
|
||||
await openFilePopup(messageFile);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -348,7 +368,7 @@ function embedMessageFile(messageId, messageBlock) {
|
||||
|
||||
await populateFileAttachment(message, 'embed_file_input');
|
||||
appendMediaToMessage(message, messageBlock);
|
||||
saveChatDebounced();
|
||||
await saveChatConditional();
|
||||
}
|
||||
}
|
||||
|
||||
@ -476,6 +496,366 @@ export function isExternalMediaAllowed() {
|
||||
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 () {
|
||||
$(document).on('click', '.mes_hide', async function () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
@ -506,6 +886,11 @@ jQuery(function () {
|
||||
$('#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 () {
|
||||
const messageBlock = $(this).closest('.mes');
|
||||
const messageId = Number(messageBlock.attr('mesid'));
|
||||
@ -597,6 +982,9 @@ jQuery(function () {
|
||||
reloadCurrentChat();
|
||||
});
|
||||
|
||||
$(document).on('click', '.mes_img_enlarge', enlargeMessageImage);
|
||||
$(document).on('click', '.mes_img_delete', deleteMessageImage);
|
||||
|
||||
$('#file_form_input').on('change', onFileAttach);
|
||||
$('#file_form').on('reset', function () {
|
||||
$('#file_form').addClass('displayNone');
|
||||
|
@ -145,6 +145,7 @@ const extension_settings = {
|
||||
variables: {
|
||||
global: {},
|
||||
},
|
||||
attachments: [],
|
||||
};
|
||||
|
||||
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>
|
||||
Generate Caption
|
||||
</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(attachFileButton);
|
||||
$(sendButton).on('click', () => {
|
||||
const hasCaptionModule =
|
||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||
|
@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const writeFileSyncAtomic = require('write-file-atomic').sync;
|
||||
const express = require('express');
|
||||
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);
|
||||
writeFileSyncAtomic(pathToUpload, request.body.data, 'base64');
|
||||
const url = clientRelativePath(request.user.directories.root, pathToUpload);
|
||||
console.log(`Uploaded file: ${url} from ${request.user.profile.handle}`);
|
||||
return response.send({ path: url });
|
||||
} catch (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 };
|
||||
|
@ -5,10 +5,10 @@ const { jsonParser } = require('../express-common');
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// Cosplay as Firefox
|
||||
// Cosplay as Chrome
|
||||
const visitHeaders = {
|
||||
'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-Encoding': 'gzip, deflate, br',
|
||||
'Connection': 'keep-alive',
|
||||
|
Loading…
x
Reference in New Issue
Block a user