diff --git a/public/script.js b/public/script.js index 101e82e98..88303011f 100644 --- a/public/script.js +++ b/public/script.js @@ -282,7 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; import { getContext } from './scripts/st-context.js'; import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; import { accountStorage } from './scripts/util/AccountStorage.js'; -import { initWelcomeScreen } from './scripts/welcome-screen.js'; +import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard } from './scripts/welcome-screen.js'; // API OBJECT FOR EXTERNAL WIRING globalThis.SillyTavern = { @@ -2041,7 +2041,7 @@ export async function sendTextareaMessage() { } if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { - await newAssistantChat(); + await newAssistantChat({ temporary: false }); } Generate(generateType); @@ -2883,7 +2883,14 @@ export async function processCommands(message) { return true; } -export function sendSystemMessage(type, text, extra = {}) { +/** + * Gets a system message by type. + * @param {string} type Type of system message + * @param {string} [text] Text to be sent + * @param {object} [extra] Additional data to be added to the message + * @returns {object} System message object + */ +export function getSystemMessageByType(type, text, extra = {}) { const systemMessage = system_messages[type]; if (!systemMessage) { @@ -2906,7 +2913,17 @@ export function sendSystemMessage(type, text, extra = {}) { newMessage.extra = Object.assign(newMessage.extra, extra); newMessage.extra.type = type; + return newMessage; +} +/** + * Sends a system message to the chat. + * @param {string} type Type of system message + * @param {string} [text] Text to be sent + * @param {object} [extra] Additional data to be added to the message + */ +export function sendSystemMessage(type, text, extra = {}) { + const newMessage = getSystemMessageByType(type, text, extra); chat.push(newMessage); addOneMessage(newMessage); is_send_press = false; @@ -10113,8 +10130,17 @@ async function removeCharacterFromUI() { saveSettingsDebounced(); } -export async function newAssistantChat() { +/** + * Creates a new assistant chat. + * @param {object} params - Parameters for the new assistant chat + * @param {boolean} [params.temporary=false] I need a temporary secretary + * @returns {Promise} - A promise that resolves when the new assistant chat is created + */ +export async function newAssistantChat({ temporary = false } = {}) { await clearChat(); + if (!temporary) { + return openPermanentAssistantChat(); + } chat.splice(0, chat.length); chat_metadata = {}; setCharacterName(neutralCharacterName); @@ -10427,7 +10453,7 @@ jQuery(async function () { if (chatId) { return reject('Not in a temporary chat'); } - await newAssistantChat(); + await newAssistantChat({ temporary: true }); return resolve(''); }; eventSource.once(event_types.CHAT_CHANGED, eventCallback); @@ -11094,6 +11120,9 @@ jQuery(async function () { }); if (id == 'option_select_chat') { + if (this_chid === undefined && !is_send_press) { + await openPermanentAssistantCard(); + } if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { await displayPastChats(); //this is just to avoid the shadow for past chat view when using /delchat @@ -11124,7 +11153,7 @@ jQuery(async function () { await doNewChat({ deleteCurrentChat: deleteCurrentChat }); } if (!selected_group && this_chid === undefined && !is_send_press) { - await newAssistantChat(); + await newAssistantChat({ temporary: true }); } } diff --git a/public/scripts/chats.js b/public/scripts/chats.js index 51b272b50..9cb753223 100644 --- a/public/scripts/chats.js +++ b/public/scripts/chats.js @@ -23,6 +23,9 @@ import { neutralCharacterName, updateChatMetadata, system_message_types, + getSystemMessageByType, + printMessages, + clearChat, } from '../script.js'; import { selected_group } from './group-chats.js'; import { power_user } from './power-user.js'; @@ -37,6 +40,7 @@ import { saveBase64AsFile, extractTextFromOffice, download, + getFileText, } from './utils.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; @@ -1497,6 +1501,35 @@ jQuery(function () { download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json'); }); + $(document).on('click', '.assistant_note_import', async function () { + const importFile = async () => { + const file = fileInput.files[0]; + if (!file) { + return; + } + + try { + const text = await getFileText(file); + const lines = text.split('\n').filter(line => line.trim() !== ''); + const messages = lines.map(line => JSON.parse(line)); + const metadata = messages.shift()?.chat_metadata || {}; + messages.unshift(getSystemMessageByType(system_message_types.ASSISTANT_NOTE)); + await clearChat(); + chat.splice(0, chat.length, ...messages); + updateChatMetadata(metadata, true); + await printMessages(); + } catch (error) { + console.error('Error importing assistant chat:', error); + toastr.error(t`It's either corrupted or not a valid JSONL file.`, t`Failed to import chat`); + } + }; + const fileInput = document.createElement('input'); + fileInput.type = 'file'; + fileInput.accept = '.jsonl'; + fileInput.addEventListener('change', importFile); + fileInput.click(); + }); + // Do not change. #attachFile is added by extension. $(document).on('click', '#attachFile', function () { $('#file_form_input').trigger('click'); diff --git a/public/scripts/templates/assistantNote.html b/public/scripts/templates/assistantNote.html index 7b2580743..2d2551e13 100644 --- a/public/scripts/templates/assistantNote.html +++ b/public/scripts/templates/assistantNote.html @@ -1,9 +1,13 @@
-
+
Note: this chat is temporary and will be deleted as soon as you leave it. - Click the button to save it as a file.
- + Save +
diff --git a/public/scripts/welcome-screen.js b/public/scripts/welcome-screen.js index 42416ea34..20bbe701d 100644 --- a/public/scripts/welcome-screen.js +++ b/public/scripts/welcome-screen.js @@ -1,21 +1,28 @@ import { characters, displayVersion, + doNewChat, event_types, eventSource, + getCharacters, getCurrentChatId, getRequestHeaders, getThumbnailUrl, + is_send_press, + neutralCharacterName, newAssistantChat, openCharacterChat, selectCharacterById, sendSystemMessage, system_message_types, } from '../script.js'; +import { is_group_generating } from './group-chats.js'; import { t } from './i18n.js'; import { renderTemplateAsync } from './templates.js'; import { timestampToMoment } from './utils.js'; +const permanentAssistantAvatar = 'default_Assistant.png'; + export async function openWelcomeScreen() { const currentChatId = getCurrentChatId(); if (currentChatId !== undefined) { @@ -28,7 +35,7 @@ export async function openWelcomeScreen() { async function sendWelcomePanel() { try { - const chatElement = document.getElementById('chat'); + const chatElement = document.getElementById('chat'); if (!chatElement) { console.error('Chat element not found'); return; @@ -36,7 +43,7 @@ async function sendWelcomePanel() { const chats = await getRecentChats(); const templateData = { chats, - empty: !chats.length , + empty: !chats.length, version: displayVersion, }; const template = await renderTemplateAsync('welcomePanel', templateData); @@ -51,7 +58,7 @@ async function sendWelcomePanel() { }); }); fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => { - void newAssistantChat(); + void newAssistantChat({ temporary: true }); }); chatElement.append(fragment.firstChild); } catch (error) { @@ -114,7 +121,7 @@ async function getRecentChats() { /** @type {RecentChat[]} */ const data = await response.json(); - data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => { + data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => { const character = characters.find(x => x.avatar === chat.avatar); if (!character) { console.warn(`Character not found for chat: ${chat.file_name}`); @@ -133,6 +140,72 @@ async function getRecentChats() { return data; } +export async function openPermanentAssistantChat({ tryCreate = true } = {}) { + const characterId = characters.findIndex(x => x.avatar === permanentAssistantAvatar); + if (characterId === -1) { + if (!tryCreate) { + console.error(`Character not found for avatar ID: ${permanentAssistantAvatar}. Cannot create.`); + return; + } + + try { + console.log(`Character not found for avatar ID: ${permanentAssistantAvatar}. Creating new assistant.`); + await createPermanentAssistant(); + return openPermanentAssistantChat({ tryCreate: false }); + } + catch (error) { + console.error('Error creating permanent assistant:', error); + toastr.error(t`Failed to create ${neutralCharacterName}. See console for details.`); + return; + } + } + + try { + await selectCharacterById(characterId); + await doNewChat({ deleteCurrentChat: false }); + console.log(`Opened permanent assistant chat for ${neutralCharacterName}.`, getCurrentChatId()); + } catch (error) { + console.error('Error opening permanent assistant chat:', error); + toastr.error(t`Failed to open permanent assistant chat. See console for details.`); + } +} + +async function createPermanentAssistant() { + if (is_group_generating || is_send_press) { + throw new Error(t`Cannot create while generating.`); + } + + const formData = new FormData(); + formData.append('ch_name', neutralCharacterName); + formData.append('file_name', permanentAssistantAvatar.replace('.png', '')); + + const headers = getRequestHeaders(); + delete headers['Content-Type']; + + const fetchResult = await fetch('/api/characters/create', { + method: 'POST', + headers: headers, + body: formData, + cache: 'no-cache', + }); + + if (!fetchResult.ok) { + throw new Error(t`Creation request did not succeed.`); + } + + await getCharacters(); +} + +export async function openPermanentAssistantCard() { + const characterId = characters.findIndex(x => x.avatar === permanentAssistantAvatar); + if (characterId === -1) { + toastr.info(t`Assistant not found. Try sending a chat message.`); + return; + } + + await selectCharacterById(characterId); +} + export function initWelcomeScreen() { const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; for (const event of events) { diff --git a/public/style.css b/public/style.css index 70a193092..eb5cd8d89 100644 --- a/public/style.css +++ b/public/style.css @@ -6061,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized { flex-wrap: nowrap; justify-content: space-between; align-items: center; - gap: 10px; - padding: 0 2px; + gap: 5px; } -.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) { +.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) > div { flex: 1; + text-align: left; } .oneline-dropdown label { diff --git a/src/endpoints/characters.js b/src/endpoints/characters.js index 6c5757cbc..fd29fc2c3 100644 --- a/src/endpoints/characters.js +++ b/src/endpoints/characters.js @@ -577,10 +577,10 @@ function charaFormatData(data, directories) { _.set(char, 'mes_example', data.mes_example || ''); // Old ST extension fields (for backward compatibility, will be deprecated) - _.set(char, 'creatorcomment', data.creator_notes); + _.set(char, 'creatorcomment', data.creator_notes || ''); _.set(char, 'avatar', 'none'); _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); - _.set(char, 'talkativeness', data.talkativeness); + _.set(char, 'talkativeness', data.talkativeness || 0.5); _.set(char, 'fav', data.fav == 'true'); _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []); @@ -604,7 +604,7 @@ function charaFormatData(data, directories) { _.set(char, 'data.alternate_greetings', getAlternateGreetings(data)); // ST extension fields to V2 object - _.set(char, 'data.extensions.talkativeness', data.talkativeness); + _.set(char, 'data.extensions.talkativeness', data.talkativeness || 0.5); _.set(char, 'data.extensions.fav', data.fav == 'true'); _.set(char, 'data.extensions.world', data.world || ''); @@ -1006,7 +1006,7 @@ router.post('/create', async function (request, response) { request.body.ch_name = sanitize(request.body.ch_name); const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); - const internalName = getPngName(request.body.ch_name, request.user.directories); + const internalName = request.body.file_name || getPngName(request.body.ch_name, request.user.directories); const avatarName = `${internalName}.png`; const chatsPath = path.join(request.user.directories.chats, internalName);