mirror of
https://github.com/SillyTavern/SillyTavern.git
synced 2025-06-05 21:59:27 +02:00
Add ability to attach files and images to messages
This commit is contained in:
57
public/css/file-form.css
Normal file
57
public/css/file-form.css
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
.file_attached {
|
||||||
|
display: flex;
|
||||||
|
min-width: 150px;
|
||||||
|
max-width: calc(var(--sheldWidth) * 0.9);
|
||||||
|
flex-direction: row;
|
||||||
|
gap: 10px;
|
||||||
|
align-items: center;
|
||||||
|
margin: 0.25em auto;
|
||||||
|
padding: 0 0.75em;
|
||||||
|
border: 2px solid var(--SmartThemeBorderColor);
|
||||||
|
border-radius: 15px;
|
||||||
|
background-color: var(--white20a);
|
||||||
|
}
|
||||||
|
|
||||||
|
.mes_file_container {
|
||||||
|
cursor: default;
|
||||||
|
display: flex;
|
||||||
|
gap: 15px;
|
||||||
|
align-items: center;
|
||||||
|
width: fit-content;
|
||||||
|
max-width: 100%;
|
||||||
|
background-color: var(--white20a);
|
||||||
|
border: 2px solid var(--SmartThemeBorderColor);
|
||||||
|
padding: 0.5em 1em;
|
||||||
|
border-radius: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mes_file_container .right_menu_button {
|
||||||
|
padding-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.mes_file_container .mes_file_size,
|
||||||
|
.file_attached .file_size {
|
||||||
|
font-size: 0.9em;
|
||||||
|
color: var(--SmartThemeQuoteColor);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_attached .file_name,
|
||||||
|
.mes_file_container .mes_file_name {
|
||||||
|
flex: 1;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
#file_form {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file_modal {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow-y: auto;
|
||||||
|
display: flex;
|
||||||
|
text-align: left;
|
||||||
|
}
|
@ -1388,9 +1388,9 @@
|
|||||||
<input id="openai_image_inlining" type="checkbox" />
|
<input id="openai_image_inlining" type="checkbox" />
|
||||||
<span data-i18n="Send inline images">Send inline images</span>
|
<span data-i18n="Send inline images">Send inline images</span>
|
||||||
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
|
<div id="image_inlining_hint" class="flexBasis100p toggle-description justifyLeft">
|
||||||
Natively replaces captioning if the model supports it (e.g. GPT-4V or Llava 13B).
|
Sends images in prompts if the model supports it (e.g. GPT-4V or Llava 13B).
|
||||||
Use the <code><i class="fa-solid fa-image"></i></code> action on any message or the
|
Use the <code><i class="fa-solid fa-paperclip"></i></code> action on any message or the
|
||||||
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image to the chat.
|
<code><i class="fa-solid fa-wand-magic-sparkles"></i></code> menu to attach an image file to the chat.
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
@ -4140,7 +4140,7 @@
|
|||||||
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt"></div>
|
<div title="Prompt" class="mes_prompt fa-solid fa-square-poll-horizontal " data-i18n="[title]Prompt"></div>
|
||||||
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
|
<div title="Exclude message from prompts" class="mes_hide fa-solid fa-eye" data-i18n="[title]Exclude message from prompts"></div>
|
||||||
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
|
<div title="Include message in prompts" class="mes_unhide fa-solid fa-eye-slash" data-i18n="[title]Include message in prompts"></div>
|
||||||
<div title="Embed image" class="mes_embed fa-solid fa-image" data-i18n="[title]Embed image"></div>
|
<div title="Embed file or image" class="mes_embed fa-solid fa-paperclip" data-i18n="[title]Embed file or image"></div>
|
||||||
<div title="Create bookmark" class="mes_create_bookmark fa-regular fa-solid fa-book-bookmark" data-i18n="[title]Create Bookmark"></div>
|
<div title="Create bookmark" class="mes_create_bookmark fa-regular fa-solid fa-book-bookmark" data-i18n="[title]Create Bookmark"></div>
|
||||||
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
|
<div title="Create branch" class="mes_create_branch fa-regular fa-code-branch" data-i18n="[title]Create Branch"></div>
|
||||||
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
|
<div title="Copy" class="mes_copy fa-solid fa-copy " data-i18n="[title]Copy"></div>
|
||||||
@ -4320,6 +4320,15 @@
|
|||||||
<div id="typing_indicator_template" class="template_element">
|
<div id="typing_indicator_template" class="template_element">
|
||||||
<div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
|
<div class="typing_indicator"><span class="typing_indicator_name">CHAR</span> is typing</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div id="message_file_template" class="template_element">
|
||||||
|
<div class="mes_file_container">
|
||||||
|
<div class="fa-lg fa-solid fa-file-alt mes_file_icon"></div>
|
||||||
|
<div class="mes_file_name"></div>
|
||||||
|
<div class="mes_file_size"></div>
|
||||||
|
<div class="right_menu_button mes_file_open fa-solid fa-magnifying-glass" title="View contents" data-i18n="[title]View contents"></div>
|
||||||
|
<div class="right_menu_button mes_file_delete fa-solid fa-trash-can" title="Remove the file" data-i18n="[title]Remove the file"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="movingDivs">
|
<div id="movingDivs">
|
||||||
<div id="floatingPrompt" class="drawer-content flexGap5">
|
<div id="floatingPrompt" class="drawer-content flexGap5">
|
||||||
<div class="panelControlBar flex-container">
|
<div class="panelControlBar flex-container">
|
||||||
@ -4607,7 +4616,19 @@
|
|||||||
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div>
|
<div id="dialogue_del_mes_ok" class="menu_button">Delete</div>
|
||||||
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>
|
<div id="dialogue_del_mes_cancel" class="menu_button">Cancel</div>
|
||||||
</div>
|
</div>
|
||||||
<form id="send_form" class="no-connection">
|
<div id="send_form" class="no-connection">
|
||||||
|
<form id="file_form" class="wide100p displayNone">
|
||||||
|
<div class="file_attached">
|
||||||
|
<input id="file_form_input" type="file" hidden>
|
||||||
|
<input id="embed_file_input" type="file" hidden>
|
||||||
|
<i class="fa-solid fa-file-alt"></i>
|
||||||
|
<span class="file_name">File Name</span>
|
||||||
|
<span class="file_size">File Size</span>
|
||||||
|
<button id="file_form_reset" type="reset" class="menu_button" title="Remove the file" data-i18n="[title]Remove the file">
|
||||||
|
<i class="fa fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
<div id="nonQRFormItems">
|
<div id="nonQRFormItems">
|
||||||
<div id="leftSendForm" class="alignContentCenter">
|
<div id="leftSendForm" class="alignContentCenter">
|
||||||
<div id="options_button" class="fa-solid fa-bars"></div>
|
<div id="options_button" class="fa-solid fa-bars"></div>
|
||||||
@ -4621,7 +4642,7 @@
|
|||||||
<div id="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
|
<div id="send_but" class="fa-solid fa-paper-plane displayNone" title="Send a message" data-i18n="[title]Send a message"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<!-- popups live outside sheld to avoid blur conflicts -->
|
<!-- popups live outside sheld to avoid blur conflicts -->
|
||||||
|
@ -145,6 +145,7 @@ import {
|
|||||||
resetScrollHeight,
|
resetScrollHeight,
|
||||||
onlyUnique,
|
onlyUnique,
|
||||||
getBase64Async,
|
getBase64Async,
|
||||||
|
humanFileSize,
|
||||||
} from "./scripts/utils.js";
|
} from "./scripts/utils.js";
|
||||||
|
|
||||||
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
|
import { ModuleWorkerWrapper, doDailyExtensionUpdatesCheck, extension_settings, getContext, loadExtensionSettings, processExtensionHelpers, registerExtensionHelper, renderExtensionTemplate, runGenerationInterceptors, saveMetadataDebounced } from "./scripts/extensions.js";
|
||||||
@ -191,6 +192,7 @@ import { getBackgrounds, initBackgrounds } from "./scripts/backgrounds.js";
|
|||||||
import { hideLoader, showLoader } from "./scripts/loader.js";
|
import { hideLoader, showLoader } from "./scripts/loader.js";
|
||||||
import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js";
|
import { CharacterContextMenu, BulkEditOverlay } from "./scripts/BulkEditOverlay.js";
|
||||||
import { loadMancerModels } from "./scripts/mancer-settings.js";
|
import { loadMancerModels } from "./scripts/mancer-settings.js";
|
||||||
|
import { hasPendingFileAttachment, populateFileAttachment } from "./scripts/chats.js";
|
||||||
|
|
||||||
//exporting functions and vars for mods
|
//exporting functions and vars for mods
|
||||||
export {
|
export {
|
||||||
@ -1329,6 +1331,28 @@ async function printMessages() {
|
|||||||
const item = chat[i];
|
const item = chat[i];
|
||||||
addOneMessage(item, { scroll: i === chat.length - 1 });
|
addOneMessage(item, { scroll: i === chat.length - 1 });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom when all images are loaded
|
||||||
|
const images = document.querySelectorAll('#chat .mes img');
|
||||||
|
let imagesLoaded = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < images.length; i++) {
|
||||||
|
const image = images[i];
|
||||||
|
if (image instanceof HTMLImageElement) {
|
||||||
|
if (image.complete) {
|
||||||
|
incrementAndCheck();
|
||||||
|
} else {
|
||||||
|
image.addEventListener('load', incrementAndCheck);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function incrementAndCheck() {
|
||||||
|
imagesLoaded++;
|
||||||
|
if (imagesLoaded === images.length) {
|
||||||
|
scrollChatToBottom();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clearChat() {
|
async function clearChat() {
|
||||||
@ -1569,10 +1593,11 @@ export function updateMessageBlock(messageId, message) {
|
|||||||
const text = message?.extra?.display_text ?? message.mes;
|
const text = message?.extra?.display_text ?? message.mes;
|
||||||
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user));
|
messageElement.find('.mes_text').html(messageFormatting(text, message.name, message.is_system, message.is_user));
|
||||||
addCopyToCodeBlocks(messageElement)
|
addCopyToCodeBlocks(messageElement)
|
||||||
appendImageToMessage(message, messageElement);
|
appendMediaToMessage(message, messageElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function appendImageToMessage(mes, messageElement) {
|
export function appendMediaToMessage(mes, messageElement) {
|
||||||
|
// Add image to message
|
||||||
if (mes.extra?.image) {
|
if (mes.extra?.image) {
|
||||||
const image = messageElement.find('.mes_img');
|
const image = messageElement.find('.mes_img');
|
||||||
const text = messageElement.find('.mes_text');
|
const text = messageElement.find('.mes_text');
|
||||||
@ -1583,6 +1608,27 @@ export function appendImageToMessage(mes, messageElement) {
|
|||||||
image.toggleClass("img_inline", isInline);
|
image.toggleClass("img_inline", isInline);
|
||||||
text.toggleClass('displayNone', !isInline);
|
text.toggleClass('displayNone', !isInline);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Add file to message
|
||||||
|
if (mes.extra?.file) {
|
||||||
|
messageElement.find(".mes_file_container").remove();
|
||||||
|
const messageId = messageElement.attr('mesid');
|
||||||
|
const template = $('#message_file_template .mes_file_container').clone();
|
||||||
|
template.find('.mes_file_name').text(mes.extra.file.name);
|
||||||
|
template.find('.mes_file_size').text(humanFileSize(mes.extra.file.size));
|
||||||
|
template.find('.mes_file_download').attr('mesid', messageId);
|
||||||
|
template.find('.mes_file_delete').attr('mesid', messageId);
|
||||||
|
messageElement.find(".mes_block").append(template);
|
||||||
|
} else {
|
||||||
|
messageElement.find(".mes_file_container").remove();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @deprecated Use appendMediaToMessage instead.
|
||||||
|
*/
|
||||||
|
export function appendImageToMessage(mes, messageElement) {
|
||||||
|
appendMediaToMessage(mes, messageElement);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function addCopyToCodeBlocks(messageElement) {
|
export function addCopyToCodeBlocks(messageElement) {
|
||||||
@ -1774,7 +1820,7 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
|
|||||||
const swipeMessage = $("#chat").find(`[mesid="${count_view_mes - 1}"]`);
|
const swipeMessage = $("#chat").find(`[mesid="${count_view_mes - 1}"]`);
|
||||||
swipeMessage.find('.mes_text').html('');
|
swipeMessage.find('.mes_text').html('');
|
||||||
swipeMessage.find('.mes_text').append(messageText);
|
swipeMessage.find('.mes_text').append(messageText);
|
||||||
appendImageToMessage(mes, swipeMessage);
|
appendMediaToMessage(mes, swipeMessage);
|
||||||
swipeMessage.attr('title', title);
|
swipeMessage.attr('title', title);
|
||||||
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
|
swipeMessage.find('.timestamp').text(timestamp).attr('title', `${params.extra.api} - ${params.extra.model}`);
|
||||||
if (power_user.timestamp_model_icon && params.extra?.api) {
|
if (power_user.timestamp_model_icon && params.extra?.api) {
|
||||||
@ -1791,12 +1837,12 @@ function addOneMessage(mes, { type = "normal", insertAfter = null, scroll = true
|
|||||||
}
|
}
|
||||||
} else if (typeof forceId == 'number') {
|
} else if (typeof forceId == 'number') {
|
||||||
$("#chat").find(`[mesid="${forceId}"]`).find('.mes_text').append(messageText);
|
$("#chat").find(`[mesid="${forceId}"]`).find('.mes_text').append(messageText);
|
||||||
appendImageToMessage(mes, newMessage);
|
appendMediaToMessage(mes, newMessage);
|
||||||
hideSwipeButtons();
|
hideSwipeButtons();
|
||||||
showSwipeButtons();
|
showSwipeButtons();
|
||||||
} else {
|
} else {
|
||||||
$("#chat").find(`[mesid="${count_view_mes}"]`).find('.mes_text').append(messageText);
|
$("#chat").find(`[mesid="${count_view_mes}"]`).find('.mes_text').append(messageText);
|
||||||
appendImageToMessage(mes, newMessage);
|
appendMediaToMessage(mes, newMessage);
|
||||||
hideSwipeButtons();
|
hideSwipeButtons();
|
||||||
count_view_mes++;
|
count_view_mes++;
|
||||||
}
|
}
|
||||||
@ -2814,7 +2860,7 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
|
|||||||
//*********************************
|
//*********************************
|
||||||
|
|
||||||
//for normal messages sent from user..
|
//for normal messages sent from user..
|
||||||
if (textareaText != "" && !automatic_trigger && type !== 'quiet') {
|
if ((textareaText != "" || hasPendingFileAttachment()) && !automatic_trigger && type !== 'quiet') {
|
||||||
// If user message contains no text other than bias - send as a system message
|
// If user message contains no text other than bias - send as a system message
|
||||||
if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) {
|
if (messageBias && replaceBiasMarkup(textareaText).trim().length === 0) {
|
||||||
sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
|
sendSystemMessage(system_message_types.GENERIC, ' ', { bias: messageBias });
|
||||||
@ -2886,12 +2932,22 @@ async function Generate(type, { automatic_trigger, force_name2, resolve, reject,
|
|||||||
coreChat.pop();
|
coreChat.pop();
|
||||||
}
|
}
|
||||||
|
|
||||||
coreChat = coreChat.map(x => ({
|
coreChat = coreChat.map(chatItem => {
|
||||||
...x,
|
let message = chatItem.mes;
|
||||||
mes: getRegexedString(x.mes, x.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT, {
|
let regexType = chatItem.is_user ? regex_placement.USER_INPUT : regex_placement.AI_OUTPUT;
|
||||||
isPrompt: true,
|
let options = { isPrompt: true };
|
||||||
}),
|
|
||||||
}))
|
let regexedMessage = getRegexedString(message, regexType, options);
|
||||||
|
|
||||||
|
if (chatItem.extra?.file?.text) {
|
||||||
|
regexedMessage += `\n\n${chatItem.extra.file.text}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
...chatItem,
|
||||||
|
mes: regexedMessage,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
// Determine token limit
|
// Determine token limit
|
||||||
let this_max_context = getMaxContextSize();
|
let this_max_context = getMaxContextSize();
|
||||||
@ -3862,6 +3918,7 @@ export async function sendMessageAsUser(textareaText, messageBias) {
|
|||||||
console.debug('checking bias');
|
console.debug('checking bias');
|
||||||
chat[chat.length - 1]['extra']['bias'] = messageBias;
|
chat[chat.length - 1]['extra']['bias'] = messageBias;
|
||||||
}
|
}
|
||||||
|
await populateFileAttachment(chat[chat.length - 1]);
|
||||||
statMesProcess(chat[chat.length - 1], 'user', characters, this_chid, '');
|
statMesProcess(chat[chat.length - 1], 'user', characters, this_chid, '');
|
||||||
// Wait for all handlers to finish before continuing with the prompt
|
// Wait for all handlers to finish before continuing with the prompt
|
||||||
const chat_id = (chat.length - 1);
|
const chat_id = (chat.length - 1);
|
||||||
@ -5603,7 +5660,7 @@ async function messageEditDone(div) {
|
|||||||
);
|
);
|
||||||
mesBlock.find(".mes_bias").empty();
|
mesBlock.find(".mes_bias").empty();
|
||||||
mesBlock.find(".mes_bias").append(messageFormatting(bias));
|
mesBlock.find(".mes_bias").append(messageFormatting(bias));
|
||||||
appendImageToMessage(mes, div.closest(".mes"));
|
appendMediaToMessage(mes, div.closest(".mes"));
|
||||||
addCopyToCodeBlocks(div.closest(".mes"));
|
addCopyToCodeBlocks(div.closest(".mes"));
|
||||||
await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id);
|
await eventSource.emit(event_types.MESSAGE_EDITED, this_edit_mes_id);
|
||||||
|
|
||||||
@ -6436,6 +6493,8 @@ function enlargeMessageImage() {
|
|||||||
imgContainer.prepend(img);
|
imgContainer.prepend(img);
|
||||||
imgContainer.addClass('img_enlarged_container');
|
imgContainer.addClass('img_enlarged_container');
|
||||||
imgContainer.find('code').addClass('txt').text(title);
|
imgContainer.find('code').addClass('txt').text(title);
|
||||||
|
const titleEmpty = !title || title.trim().length === 0;
|
||||||
|
imgContainer.find('pre').toggle(!titleEmpty);
|
||||||
addCopyToCodeBlocks(imgContainer);
|
addCopyToCodeBlocks(imgContainer);
|
||||||
callPopup(imgContainer, 'text', '', { wide: true, large: true });
|
callPopup(imgContainer, 'text', '', { wide: true, large: true });
|
||||||
}
|
}
|
||||||
@ -8538,7 +8597,7 @@ jQuery(async function () {
|
|||||||
chat[this_edit_mes_id].is_system,
|
chat[this_edit_mes_id].is_system,
|
||||||
chat[this_edit_mes_id].is_user,
|
chat[this_edit_mes_id].is_user,
|
||||||
));
|
));
|
||||||
appendImageToMessage(chat[this_edit_mes_id], $(this).closest(".mes"));
|
appendMediaToMessage(chat[this_edit_mes_id], $(this).closest(".mes"));
|
||||||
addCopyToCodeBlocks($(this).closest(".mes"));
|
addCopyToCodeBlocks($(this).closest(".mes"));
|
||||||
this_edit_mes_id = undefined;
|
this_edit_mes_id = undefined;
|
||||||
});
|
});
|
||||||
|
@ -1,12 +1,21 @@
|
|||||||
// Move chat functions here from script.js (eventually)
|
// Move chat functions here from script.js (eventually)
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
addCopyToCodeBlocks,
|
||||||
|
appendMediaToMessage,
|
||||||
|
callPopup,
|
||||||
chat,
|
chat,
|
||||||
|
eventSource,
|
||||||
|
event_types,
|
||||||
getCurrentChatId,
|
getCurrentChatId,
|
||||||
hideSwipeButtons,
|
hideSwipeButtons,
|
||||||
|
name2,
|
||||||
saveChatDebounced,
|
saveChatDebounced,
|
||||||
showSwipeButtons,
|
showSwipeButtons,
|
||||||
} from "../script.js";
|
} from "../script.js";
|
||||||
|
import { getBase64Async, humanFileSize, saveBase64AsFile } from "./utils.js";
|
||||||
|
|
||||||
|
const fileSizeLimit = 1024 * 1024 * 1; // 1 MB
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Mark message as hidden (system message).
|
* Mark message as hidden (system message).
|
||||||
@ -58,16 +67,222 @@ export async function unhideChatMessage(messageId, messageBlock) {
|
|||||||
saveChatDebounced();
|
saveChatDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
jQuery(function() {
|
/**
|
||||||
$(document).on('click', '.mes_hide', async function() {
|
* Adds a file attachment to the message.
|
||||||
|
* @param {object} message Message object
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
export async function populateFileAttachment(message, inputId = 'file_form_input') {
|
||||||
|
try {
|
||||||
|
if (!message) return;
|
||||||
|
if (!message.extra) message.extra = {};
|
||||||
|
const fileInput = document.getElementById(inputId);
|
||||||
|
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
// If file is image
|
||||||
|
if (file.type.startsWith('image/')) {
|
||||||
|
const base64Img = await getBase64Async(file);
|
||||||
|
const base64ImgData = base64Img.split(',')[1];
|
||||||
|
const extension = file.type.split('/')[1];
|
||||||
|
const imageUrl = await saveBase64AsFile(base64ImgData, name2, file.name, extension);
|
||||||
|
message.extra.image = imageUrl;
|
||||||
|
message.extra.inline_image = true;
|
||||||
|
} else {
|
||||||
|
const fileText = await file.text();
|
||||||
|
message.extra.file = {
|
||||||
|
text: fileText,
|
||||||
|
size: file.size,
|
||||||
|
name: file.name,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Could not upload file', error);
|
||||||
|
} finally {
|
||||||
|
$('#file_form').trigger('reset');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validates file to make sure it is not binary or not image.
|
||||||
|
* @param {File} file File object
|
||||||
|
* @returns {Promise<boolean>} True if file is valid, false otherwise.
|
||||||
|
*/
|
||||||
|
async function validateFile(file) {
|
||||||
|
const fileText = await file.text();
|
||||||
|
const isImage = file.type.startsWith('image/');
|
||||||
|
const isBinary = /^[\x00-\x08\x0E-\x1F\x7F-\xFF]*$/.test(fileText);
|
||||||
|
|
||||||
|
if (!isImage && file.size > fileSizeLimit) {
|
||||||
|
toastr.error(`File is too big. Maximum size is ${humanFileSize(fileSizeLimit)}.`);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// If file is binary
|
||||||
|
if (isBinary && !isImage) {
|
||||||
|
toastr.error('Binary files are not supported. Select a text file or image.');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function hasPendingFileAttachment() {
|
||||||
|
const fileInput = document.getElementById('file_form_input');
|
||||||
|
if (!(fileInput instanceof HTMLInputElement)) return false;
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
return !!file;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Displays file information in the message sending form.
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
async function onFileAttach() {
|
||||||
|
const fileInput = document.getElementById('file_form_input');
|
||||||
|
if (!(fileInput instanceof HTMLInputElement)) return;
|
||||||
|
const file = fileInput.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const isValid = await validateFile(file);
|
||||||
|
|
||||||
|
// If file is binary
|
||||||
|
if (!isValid) {
|
||||||
|
$('#file_form').trigger('reset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#file_form .file_name').text(file.name);
|
||||||
|
$('#file_form .file_size').text(humanFileSize(file.size));
|
||||||
|
$('#file_form').removeClass('displayNone');
|
||||||
|
|
||||||
|
// Reset form on chat change
|
||||||
|
eventSource.once(event_types.CHAT_CHANGED, () => {
|
||||||
|
$('#file_form').trigger('reset');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes file from message.
|
||||||
|
* @param {number} messageId Message ID
|
||||||
|
*/
|
||||||
|
async function deleteMessageFile(messageId) {
|
||||||
|
const confirm = await callPopup('Are you sure you want to delete this file?', 'confirm');
|
||||||
|
|
||||||
|
if (!confirm) {
|
||||||
|
console.debug('Delete file cancelled');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const message = chat[messageId];
|
||||||
|
|
||||||
|
if (!message?.extra?.file) {
|
||||||
|
console.debug('Message has no file');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
delete message.extra.file;
|
||||||
|
$(`.mes[mesid="${messageId}"] .mes_file_container`).remove();
|
||||||
|
saveChatDebounced();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Opens file from message in a modal.
|
||||||
|
* @param {number} messageId Message ID
|
||||||
|
*/
|
||||||
|
async function viewMessageFile(messageId) {
|
||||||
|
const messageText = chat[messageId]?.extra?.file?.text;
|
||||||
|
|
||||||
|
if (!messageText) {
|
||||||
|
console.debug('Message has no file or it is empty');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const modalTemplate = $('<div><pre><code></code></pre></div>');
|
||||||
|
modalTemplate.find('code').addClass('txt').text(messageText);
|
||||||
|
modalTemplate.addClass('file_modal');
|
||||||
|
addCopyToCodeBlocks(modalTemplate);
|
||||||
|
|
||||||
|
callPopup(modalTemplate, 'text');
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Inserts a file embed into the message.
|
||||||
|
* @param {number} messageId
|
||||||
|
* @param {JQuery<HTMLElement>} messageBlock
|
||||||
|
* @returns {Promise<void>}
|
||||||
|
*/
|
||||||
|
function embedMessageFile(messageId, messageBlock) {
|
||||||
|
const message = chat[messageId];
|
||||||
|
|
||||||
|
if (!message) {
|
||||||
|
console.warn('Failed to find message with id', messageId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$('#embed_file_input')
|
||||||
|
.off('change')
|
||||||
|
.on('change', parseAndUploadEmbed)
|
||||||
|
.trigger('click');
|
||||||
|
|
||||||
|
async function parseAndUploadEmbed(e) {
|
||||||
|
const file = e.target.files[0];
|
||||||
|
if (!file) return;
|
||||||
|
|
||||||
|
const isValid = await validateFile(file);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
$('#file_form').trigger('reset');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await populateFileAttachment(message, 'embed_file_input');
|
||||||
|
appendMediaToMessage(message, messageBlock);
|
||||||
|
saveChatDebounced();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jQuery(function () {
|
||||||
|
$(document).on('click', '.mes_hide', async function () {
|
||||||
const messageBlock = $(this).closest('.mes');
|
const messageBlock = $(this).closest('.mes');
|
||||||
const messageId = Number(messageBlock.attr('mesid'));
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
await hideChatMessage(messageId, messageBlock);
|
await hideChatMessage(messageId, messageBlock);
|
||||||
});
|
});
|
||||||
|
|
||||||
$(document).on('click', '.mes_unhide', async function() {
|
$(document).on('click', '.mes_unhide', async function () {
|
||||||
const messageBlock = $(this).closest('.mes');
|
const messageBlock = $(this).closest('.mes');
|
||||||
const messageId = Number(messageBlock.attr('mesid'));
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
await unhideChatMessage(messageId, messageBlock);
|
await unhideChatMessage(messageId, messageBlock);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.mes_file_delete', async function () {
|
||||||
|
const messageBlock = $(this).closest('.mes');
|
||||||
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
|
await deleteMessageFile(messageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.mes_file_open', async function () {
|
||||||
|
const messageBlock = $(this).closest('.mes');
|
||||||
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
|
await viewMessageFile(messageId);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Do not change. #attachFile is added by extension.
|
||||||
|
$(document).on('click', '#attachFile', function () {
|
||||||
|
$('#file_form_input').trigger('click');
|
||||||
|
});
|
||||||
|
|
||||||
|
$(document).on('click', '.mes_embed', function () {
|
||||||
|
const messageBlock = $(this).closest('.mes');
|
||||||
|
const messageId = Number(messageBlock.attr('mesid'));
|
||||||
|
embedMessageFile(messageId, messageBlock);
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#file_form_input').on('change', onFileAttach);
|
||||||
|
$('#file_form').on('reset', function () {
|
||||||
|
$('#file_form').addClass('displayNone');
|
||||||
|
});
|
||||||
})
|
})
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import { getBase64Async, saveBase64AsFile } from "../../utils.js";
|
import { getBase64Async, saveBase64AsFile } from "../../utils.js";
|
||||||
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
|
import { getContext, getApiUrl, doExtrasFetch, extension_settings, modules } from "../../extensions.js";
|
||||||
import { appendImageToMessage, callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
|
import { callPopup, getRequestHeaders, saveSettingsDebounced, substituteParams } from "../../../script.js";
|
||||||
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
|
import { getMessageTimeStamp } from "../../RossAscends-mods.js";
|
||||||
import { SECRET_KEYS, secret_state } from "../../secrets.js";
|
import { SECRET_KEYS, secret_state } from "../../secrets.js";
|
||||||
import { isImageInliningSupported } from "../../openai.js";
|
|
||||||
import { getMultimodalCaption } from "../shared.js";
|
import { getMultimodalCaption } from "../shared.js";
|
||||||
export { MODULE_NAME };
|
export { MODULE_NAME };
|
||||||
|
|
||||||
@ -118,7 +117,6 @@ async function sendCaptionedMessage(caption, image) {
|
|||||||
};
|
};
|
||||||
context.chat.push(message);
|
context.chat.push(message);
|
||||||
context.addOneMessage(message);
|
context.addOneMessage(message);
|
||||||
await context.generate('caption');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -255,99 +253,22 @@ function onRefineModeInput() {
|
|||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
}
|
}
|
||||||
|
|
||||||
async function sendEmbeddedImage(e) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
|
|
||||||
if (!file || !(file instanceof File)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const context = getContext();
|
|
||||||
const fileData = await getBase64Async(file);
|
|
||||||
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
|
|
||||||
const base64Data = fileData.split(',')[1];
|
|
||||||
const caption = await callPopup('<h3>Enter a comment or question (optional)</h3>', 'input', 'What is this?', { okButton: 'Send', rows: 2 });
|
|
||||||
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
|
|
||||||
const message = {
|
|
||||||
name: context.name1,
|
|
||||||
is_user: true,
|
|
||||||
send_date: getMessageTimeStamp(),
|
|
||||||
mes: caption || `[${context.name1} sends ${context.name2} a picture]`,
|
|
||||||
extra: {
|
|
||||||
image: imagePath,
|
|
||||||
inline_image: !!caption,
|
|
||||||
title: caption || '',
|
|
||||||
},
|
|
||||||
};
|
|
||||||
context.chat.push(message);
|
|
||||||
context.addOneMessage(message);
|
|
||||||
await context.generate('caption');
|
|
||||||
}
|
|
||||||
catch (error) {
|
|
||||||
console.log(error);
|
|
||||||
}
|
|
||||||
finally {
|
|
||||||
e.target.form.reset();
|
|
||||||
setImageIcon();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageEmbedClicked() {
|
|
||||||
const context = getContext();
|
|
||||||
const messageElement = $(this).closest('.mes');
|
|
||||||
const messageId = messageElement.attr('mesid');
|
|
||||||
const message = context.chat[messageId];
|
|
||||||
|
|
||||||
if (!message) {
|
|
||||||
console.warn('Failed to find message with id', messageId);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$('#embed_img_file')
|
|
||||||
.off('change')
|
|
||||||
.on('change', parseAndUploadEmbed)
|
|
||||||
.trigger('click');
|
|
||||||
|
|
||||||
async function parseAndUploadEmbed(e) {
|
|
||||||
const file = e.target.files[0];
|
|
||||||
|
|
||||||
if (!file || !(file instanceof File)) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const fileData = await getBase64Async(file);
|
|
||||||
const base64Data = fileData.split(',')[1];
|
|
||||||
const base64Format = fileData.split(',')[0].split(';')[0].split('/')[1];
|
|
||||||
const imagePath = await saveBase64AsFile(base64Data, context.name2, '', base64Format);
|
|
||||||
|
|
||||||
if (!message.extra) {
|
|
||||||
message.extra = {};
|
|
||||||
}
|
|
||||||
|
|
||||||
message.extra.image = imagePath;
|
|
||||||
message.extra.inline_image = true;
|
|
||||||
message.extra.title = '';
|
|
||||||
appendImageToMessage(message, messageElement);
|
|
||||||
await context.saveChat();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
jQuery(function () {
|
jQuery(function () {
|
||||||
function addSendPictureButton() {
|
function addSendPictureButton() {
|
||||||
const sendButton = $(`
|
const sendButton = $(`
|
||||||
<div id="send_picture" class="list-group-item flex-container flexGap5">
|
<div id="send_picture" class="list-group-item flex-container flexGap5">
|
||||||
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
<div class="fa-solid fa-image extensionsMenuExtensionButton"></div>
|
||||||
Send a Picture
|
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>`);
|
</div>`);
|
||||||
|
|
||||||
$('#extensionsMenu').prepend(sendButton);
|
$('#extensionsMenu').prepend(sendButton);
|
||||||
|
$('#extensionsMenu').prepend(attachFileButton);
|
||||||
$(sendButton).on('click', () => {
|
$(sendButton).on('click', () => {
|
||||||
if (isImageInliningSupported()) {
|
|
||||||
console.log('Native image inlining is supported. Skipping captioning.');
|
|
||||||
$('#embed_img_file').off('change').on('change', sendEmbeddedImage).trigger('click');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasCaptionModule =
|
const hasCaptionModule =
|
||||||
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
(modules.includes('caption') && extension_settings.caption.source === 'extras') ||
|
||||||
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
|
(extension_settings.caption.source === 'multimodal' && extension_settings.caption.multimodal_api === 'openai' && secret_state[SECRET_KEYS.OPENAI]) ||
|
||||||
@ -365,11 +286,9 @@ jQuery(function () {
|
|||||||
}
|
}
|
||||||
function addPictureSendForm() {
|
function addPictureSendForm() {
|
||||||
const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
|
const inputHtml = `<input id="img_file" type="file" hidden accept="image/*">`;
|
||||||
const embedInputHtml = `<input id="embed_img_file" type="file" hidden accept="image/*">`;
|
|
||||||
const imgForm = document.createElement('form');
|
const imgForm = document.createElement('form');
|
||||||
imgForm.id = 'img_form';
|
imgForm.id = 'img_form';
|
||||||
$(imgForm).append(inputHtml);
|
$(imgForm).append(inputHtml);
|
||||||
$(imgForm).append(embedInputHtml);
|
|
||||||
$(imgForm).hide();
|
$(imgForm).hide();
|
||||||
$('#form_sheld').append(imgForm);
|
$('#form_sheld').append(imgForm);
|
||||||
$('#img_file').on('change', onSelectImage);
|
$('#img_file').on('change', onSelectImage);
|
||||||
@ -472,5 +391,4 @@ jQuery(function () {
|
|||||||
extension_settings.caption.template = String($('#caption_template').val());
|
extension_settings.caption.template = String($('#caption_template').val());
|
||||||
saveSettingsDebounced();
|
saveSettingsDebounced();
|
||||||
});
|
});
|
||||||
$(document).on('click', '.mes_embed', onImageEmbedClicked);
|
|
||||||
});
|
});
|
||||||
|
@ -365,7 +365,7 @@ async function sendUserMessageCallback(_, text) {
|
|||||||
|
|
||||||
text = text.trim();
|
text = text.trim();
|
||||||
const bias = extractMessageBias(text);
|
const bias = extractMessageBias(text);
|
||||||
sendMessageAsUser(text, bias);
|
await sendMessageAsUser(text, bias);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteMessagesByNameCallback(_, name) {
|
async function deleteMessagesByNameCallback(_, name) {
|
||||||
|
@ -512,6 +512,38 @@ export function trimToStartSentence(input) {
|
|||||||
return input;
|
return input;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Format bytes as human-readable text.
|
||||||
|
*
|
||||||
|
* @param bytes Number of bytes.
|
||||||
|
* @param si True to use metric (SI) units, aka powers of 1000. False to use
|
||||||
|
* binary (IEC), aka powers of 1024.
|
||||||
|
* @param dp Number of decimal places to display.
|
||||||
|
*
|
||||||
|
* @return Formatted string.
|
||||||
|
*/
|
||||||
|
export function humanFileSize(bytes, si = false, dp = 1) {
|
||||||
|
const thresh = si ? 1000 : 1024;
|
||||||
|
|
||||||
|
if (Math.abs(bytes) < thresh) {
|
||||||
|
return bytes + ' B';
|
||||||
|
}
|
||||||
|
|
||||||
|
const units = si
|
||||||
|
? ['kB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']
|
||||||
|
: ['KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
|
||||||
|
let u = -1;
|
||||||
|
const r = 10 ** dp;
|
||||||
|
|
||||||
|
do {
|
||||||
|
bytes /= thresh;
|
||||||
|
++u;
|
||||||
|
} while (Math.round(Math.abs(bytes) * r) / r >= thresh && u < units.length - 1);
|
||||||
|
|
||||||
|
|
||||||
|
return bytes.toFixed(dp) + ' ' + units[u];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Counts the number of occurrences of a character in a string.
|
* Counts the number of occurrences of a character in a string.
|
||||||
* @param {string} string The string to count occurrences in.
|
* @param {string} string The string to count occurrences in.
|
||||||
|
@ -3,6 +3,7 @@
|
|||||||
@import url(css/promptmanager.css);
|
@import url(css/promptmanager.css);
|
||||||
@import url(css/loader.css);
|
@import url(css/loader.css);
|
||||||
@import url(css/character-group-overlay.css);
|
@import url(css/character-group-overlay.css);
|
||||||
|
@import url(css/file-form.css);
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--doc-height: 100%;
|
--doc-height: 100%;
|
||||||
@ -757,6 +758,10 @@ hr {
|
|||||||
/*only affects bubblechat to make it sit nicely at the bottom*/
|
/*only affects bubblechat to make it sit nicely at the bottom*/
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.last_mes .mes_text {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
|
||||||
/* SWIPE RELATED STYLES*/
|
/* SWIPE RELATED STYLES*/
|
||||||
|
|
||||||
.swipe_right,
|
.swipe_right,
|
||||||
@ -925,6 +930,7 @@ hr {
|
|||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-left: 10px;
|
padding-left: 10px;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
overflow-x: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mes_text {
|
.mes_text {
|
||||||
@ -2485,14 +2491,6 @@ input[type="range"]::-webkit-slider-thumb {
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.last_mes .mes_buttons {
|
|
||||||
right: -30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.last_mes .mes_block {
|
|
||||||
margin-right: 30px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.mes_buttons .mes_edit,
|
.mes_buttons .mes_edit,
|
||||||
.mes_buttons .mes_bookmark,
|
.mes_buttons .mes_bookmark,
|
||||||
.mes_buttons .mes_create_bookmark,
|
.mes_buttons .mes_create_bookmark,
|
||||||
|
Reference in New Issue
Block a user