Permanent assistant autocreation and temporary chat restore

This commit is contained in:
Cohee
2025-05-12 02:14:54 +03:00
parent 61f69aa674
commit 31e2cf714a
6 changed files with 160 additions and 21 deletions

View File

@ -282,7 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
import { getContext } from './scripts/st-context.js'; import { getContext } from './scripts/st-context.js';
import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
import { accountStorage } from './scripts/util/AccountStorage.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 // API OBJECT FOR EXTERNAL WIRING
globalThis.SillyTavern = { globalThis.SillyTavern = {
@ -2041,7 +2041,7 @@ export async function sendTextareaMessage() {
} }
if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) {
await newAssistantChat(); await newAssistantChat({ temporary: false });
} }
Generate(generateType); Generate(generateType);
@ -2883,7 +2883,14 @@ export async function processCommands(message) {
return true; 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]; const systemMessage = system_messages[type];
if (!systemMessage) { if (!systemMessage) {
@ -2906,7 +2913,17 @@ export function sendSystemMessage(type, text, extra = {}) {
newMessage.extra = Object.assign(newMessage.extra, extra); newMessage.extra = Object.assign(newMessage.extra, extra);
newMessage.extra.type = type; 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); chat.push(newMessage);
addOneMessage(newMessage); addOneMessage(newMessage);
is_send_press = false; is_send_press = false;
@ -10113,8 +10130,17 @@ async function removeCharacterFromUI() {
saveSettingsDebounced(); 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<void>} - A promise that resolves when the new assistant chat is created
*/
export async function newAssistantChat({ temporary = false } = {}) {
await clearChat(); await clearChat();
if (!temporary) {
return openPermanentAssistantChat();
}
chat.splice(0, chat.length); chat.splice(0, chat.length);
chat_metadata = {}; chat_metadata = {};
setCharacterName(neutralCharacterName); setCharacterName(neutralCharacterName);
@ -10427,7 +10453,7 @@ jQuery(async function () {
if (chatId) { if (chatId) {
return reject('Not in a temporary chat'); return reject('Not in a temporary chat');
} }
await newAssistantChat(); await newAssistantChat({ temporary: true });
return resolve(''); return resolve('');
}; };
eventSource.once(event_types.CHAT_CHANGED, eventCallback); eventSource.once(event_types.CHAT_CHANGED, eventCallback);
@ -11094,6 +11120,9 @@ jQuery(async function () {
}); });
if (id == 'option_select_chat') { 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) { if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {
await displayPastChats(); await displayPastChats();
//this is just to avoid the shadow for past chat view when using /delchat //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 }); await doNewChat({ deleteCurrentChat: deleteCurrentChat });
} }
if (!selected_group && this_chid === undefined && !is_send_press) { if (!selected_group && this_chid === undefined && !is_send_press) {
await newAssistantChat(); await newAssistantChat({ temporary: true });
} }
} }

View File

@ -23,6 +23,9 @@ import {
neutralCharacterName, neutralCharacterName,
updateChatMetadata, updateChatMetadata,
system_message_types, system_message_types,
getSystemMessageByType,
printMessages,
clearChat,
} 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';
@ -37,6 +40,7 @@ import {
saveBase64AsFile, saveBase64AsFile,
extractTextFromOffice, extractTextFromOffice,
download, download,
getFileText,
} from './utils.js'; } from './utils.js';
import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.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'); 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. // Do not change. #attachFile is added by extension.
$(document).on('click', '#attachFile', function () { $(document).on('click', '#attachFile', function () {
$('#file_form_input').trigger('click'); $('#file_form_input').trigger('click');

View File

@ -1,9 +1,13 @@
<div data-type="assistant_note"> <div data-type="assistant_note">
<div> <div class="assistant_note_title">
<b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span> <b data-i18n="Note:">Note:</b> <span data-i18n="this chat is temporary and will be deleted as soon as you leave it.">this chat is temporary and will be deleted as soon as you leave it.</span>
<span data-i18n="Click the button to save it as a file.">Click the button to save it as a file.</span>
</div> </div>
<div class="assistant_note_export menu_button menu_button_icon" data-i18n="[title]Export as JSONL" title="Export as JSONL"> <button class="assistant_note_import menu_button menu_button_icon margin0" data-i18n="[title]Import from JSONL" title="Import from JSONL">
<i class="fa-solid fa-file-import"></i>
<span data-i18n="Load">Load</span>
</button>
<button class="assistant_note_export menu_button menu_button_icon margin0" data-i18n="[title]Export as JSONL" title="Export as JSONL">
<i class="fa-solid fa-file-export"></i> <i class="fa-solid fa-file-export"></i>
</div> <span data-i18n="Save">Save</span>
</button>
</div> </div>

View File

@ -1,21 +1,28 @@
import { import {
characters, characters,
displayVersion, displayVersion,
doNewChat,
event_types, event_types,
eventSource, eventSource,
getCharacters,
getCurrentChatId, getCurrentChatId,
getRequestHeaders, getRequestHeaders,
getThumbnailUrl, getThumbnailUrl,
is_send_press,
neutralCharacterName,
newAssistantChat, newAssistantChat,
openCharacterChat, openCharacterChat,
selectCharacterById, selectCharacterById,
sendSystemMessage, sendSystemMessage,
system_message_types, system_message_types,
} from '../script.js'; } from '../script.js';
import { is_group_generating } from './group-chats.js';
import { t } from './i18n.js'; import { t } from './i18n.js';
import { renderTemplateAsync } from './templates.js'; import { renderTemplateAsync } from './templates.js';
import { timestampToMoment } from './utils.js'; import { timestampToMoment } from './utils.js';
const permanentAssistantAvatar = 'default_Assistant.png';
export async function openWelcomeScreen() { export async function openWelcomeScreen() {
const currentChatId = getCurrentChatId(); const currentChatId = getCurrentChatId();
if (currentChatId !== undefined) { if (currentChatId !== undefined) {
@ -51,7 +58,7 @@ async function sendWelcomePanel() {
}); });
}); });
fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => { fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => {
void newAssistantChat(); void newAssistantChat({ temporary: true });
}); });
chatElement.append(fragment.firstChild); chatElement.append(fragment.firstChild);
} catch (error) { } catch (error) {
@ -133,6 +140,72 @@ async function getRecentChats() {
return data; 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() { export function initWelcomeScreen() {
const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; const events = [event_types.CHAT_CHANGED, event_types.APP_READY];
for (const event of events) { for (const event of events) {

View File

@ -6061,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized {
flex-wrap: nowrap; flex-wrap: nowrap;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
gap: 10px; gap: 5px;
padding: 0 2px;
} }
.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; flex: 1;
text-align: left;
} }
.oneline-dropdown label { .oneline-dropdown label {

View File

@ -577,10 +577,10 @@ function charaFormatData(data, directories) {
_.set(char, 'mes_example', data.mes_example || ''); _.set(char, 'mes_example', data.mes_example || '');
// Old ST extension fields (for backward compatibility, will be deprecated) // 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, 'avatar', 'none');
_.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); _.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, 'fav', data.fav == 'true');
_.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []); _.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)); _.set(char, 'data.alternate_greetings', getAlternateGreetings(data));
// ST extension fields to V2 object // 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.fav', data.fav == 'true');
_.set(char, 'data.extensions.world', data.world || ''); _.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); request.body.ch_name = sanitize(request.body.ch_name);
const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); 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 avatarName = `${internalName}.png`;
const chatsPath = path.join(request.user.directories.chats, internalName); const chatsPath = path.join(request.user.directories.chats, internalName);