Permanent assistant autocreation and temporary chat restore

31e2cf714a43fef67f4ee2ab02a2f3b9eae14568

Cohee <18619528+Cohee1207@users.noreply.github.com>

6 files changed, +160 -21Ignore whitespace
public/script.js+35 -6
@@ -282,7 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js';
282282import { getContext } from './scripts/st-context.js';
283283import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
284284import { accountStorage } from './scripts/util/AccountStorage.js';
285285import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard } from './scripts/welcome-screen.js';
286286
287287// API OBJECT FOR EXTERNAL WIRING
288288globalThis.SillyTavern = {
@@ -2041,7 +2041,7 @@ export async function sendTextareaMessage() {
20412041 }
20422042
20432043 if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) {
20442044 await newAssistantChat({ temporary: false });
20452045 }
20462046
20472047 Generate(generateType);
@@ -2883,7 +2883,14 @@ export async function processCommands(message) {
28832883 return true;
28842884}
28852885
2886-export function sendSystemMessage(type, text, extra = {}) {
2886+/**
2887+ * Gets a system message by type.
2888+ * @param {string} type Type of system message
2889+ * @param {string} [text] Text to be sent
2890+ * @param {object} [extra] Additional data to be added to the message
2891+ * @returns {object} System message object
2892+ */
2893+export function getSystemMessageByType(type, text, extra = {}) {
28872894 const systemMessage = system_messages[type];
28882895
28892896 if (!systemMessage) {
@@ -2906,7 +2913,17 @@ export function sendSystemMessage(type, text, extra = {}) {
29062913
29072914 newMessage.extra = Object.assign(newMessage.extra, extra);
29082915 newMessage.extra.type = type;
2916+ return newMessage;
2917+}
29092918
2919+/**
2920+ * Sends a system message to the chat.
2921+ * @param {string} type Type of system message
2922+ * @param {string} [text] Text to be sent
2923+ * @param {object} [extra] Additional data to be added to the message
2924+ */
2925+export function sendSystemMessage(type, text, extra = {}) {
2926+ const newMessage = getSystemMessageByType(type, text, extra);
29102927 chat.push(newMessage);
29112928 addOneMessage(newMessage);
29122929 is_send_press = false;
@@ -10113,8 +10130,17 @@ async function removeCharacterFromUI() {
1011310130 saveSettingsDebounced();
1011410131}
1011510132
10116-export async function newAssistantChat() {
10133+/**
10134+ * Creates a new assistant chat.
10135+ * @param {object} params - Parameters for the new assistant chat
10136+ * @param {boolean} [params.temporary=false] I need a temporary secretary
10137+ * @returns {Promise<void>} - A promise that resolves when the new assistant chat is created
10138+ */
10139+export async function newAssistantChat({ temporary = false } = {}) {
1011710140 await clearChat();
10141+ if (!temporary) {
10142+ return openPermanentAssistantChat();
10143+ }
1011810144 chat.splice(0, chat.length);
1011910145 chat_metadata = {};
1012010146 setCharacterName(neutralCharacterName);
@@ -10427,7 +10453,7 @@ jQuery(async function () {
1042710453 if (chatId) {
1042810454 return reject('Not in a temporary chat');
1042910455 }
1043010456 await newAssistantChat({ temporary: true });
1043110457 return resolve('');
1043210458 };
1043310459 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
@@ -11094,6 +11120,9 @@ jQuery(async function () {
1109411120 });
1109511121
1109611122 if (id == 'option_select_chat') {
11123+ if (this_chid === undefined && !is_send_press) {
11124+ await openPermanentAssistantCard();
11125+ }
1109711126 if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {
1109811127 await displayPastChats();
1109911128 //this is just to avoid the shadow for past chat view when using /delchat
@@ -11124,7 +11153,7 @@ jQuery(async function () {
1112411153 await doNewChat({ deleteCurrentChat: deleteCurrentChat });
1112511154 }
1112611155 if (!selected_group && this_chid === undefined && !is_send_press) {
1112711156 await newAssistantChat({ temporary: true });
1112811157 }
1112911158 }
1113011159
public/scripts/chats.js+33 -0
@@ -23,6 +23,9 @@ import {
2323 neutralCharacterName,
2424 updateChatMetadata,
2525 system_message_types,
26+ getSystemMessageByType,
27+ printMessages,
28+ clearChat,
2629} from '../script.js';
2730import { selected_group } from './group-chats.js';
2831import { power_user } from './power-user.js';
@@ -37,6 +40,7 @@ import {
3740 saveBase64AsFile,
3841 extractTextFromOffice,
3942 download,
43+ getFileText,
4044} from './utils.js';
4145import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
4246import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -1497,6 +1501,35 @@ jQuery(function () {
14971501 download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
14981502 });
14991503
1504+ $(document).on('click', '.assistant_note_import', async function () {
1505+ const importFile = async () => {
1506+ const file = fileInput.files[0];
1507+ if (!file) {
1508+ return;
1509+ }
1510+
1511+ try {
1512+ const text = await getFileText(file);
1513+ const lines = text.split('\n').filter(line => line.trim() !== '');
1514+ const messages = lines.map(line => JSON.parse(line));
1515+ const metadata = messages.shift()?.chat_metadata || {};
1516+ messages.unshift(getSystemMessageByType(system_message_types.ASSISTANT_NOTE));
1517+ await clearChat();
1518+ chat.splice(0, chat.length, ...messages);
1519+ updateChatMetadata(metadata, true);
1520+ await printMessages();
1521+ } catch (error) {
1522+ console.error('Error importing assistant chat:', error);
1523+ toastr.error(t`It's either corrupted or not a valid JSONL file.`, t`Failed to import chat`);
1524+ }
1525+ };
1526+ const fileInput = document.createElement('input');
1527+ fileInput.type = 'file';
1528+ fileInput.accept = '.jsonl';
1529+ fileInput.addEventListener('change', importFile);
1530+ fileInput.click();
1531+ });
1532+
15001533 // Do not change. #attachFile is added by extension.
15011534 $(document).on('click', '#attachFile', function () {
15021535 $('#file_form_input').trigger('click');
public/scripts/templates/assistantNote.html+8 -4
@@ -1,9 +1,13 @@
11<div data-type="assistant_note">
22 <div class="assistant_note_title">
33 <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>
4- <span data-i18n="Click the button to save it as a file.">Click the button to save it as a file.</span>
54 </div>
65 <divbutton class="assistant_note_exportassistant_note_import menu_button menu_button_icon margin0" data-i18n="[title]ExportImport asfrom JSONL" title="ExportImport asfrom JSONL">
6+ <i class="fa-solid fa-file-import"></i>
7+ <span data-i18n="Load">Load</span>
8+ </button>
9+ <button class="assistant_note_export menu_button menu_button_icon margin0" data-i18n="[title]Export as JSONL" title="Export as JSONL">
710 <i class="fa-solid fa-file-export"></i>
8- </div>
11+ <span data-i18n="Save">Save</span>
12+ </button>
913</div>
public/scripts/welcome-screen.js+77 -4
@@ -1,21 +1,28 @@
11import {
22 characters,
33 displayVersion,
4+ doNewChat,
45 event_types,
56 eventSource,
7+ getCharacters,
68 getCurrentChatId,
79 getRequestHeaders,
810 getThumbnailUrl,
11+ is_send_press,
12+ neutralCharacterName,
913 newAssistantChat,
1014 openCharacterChat,
1115 selectCharacterById,
1216 sendSystemMessage,
1317 system_message_types,
1418} from '../script.js';
19+import { is_group_generating } from './group-chats.js';
1520import { t } from './i18n.js';
1621import { renderTemplateAsync } from './templates.js';
1722import { timestampToMoment } from './utils.js';
1823
24+const permanentAssistantAvatar = 'default_Assistant.png';
25+
1926export async function openWelcomeScreen() {
2027 const currentChatId = getCurrentChatId();
2128 if (currentChatId !== undefined) {
@@ -28,7 +35,7 @@ export async function openWelcomeScreen() {
2835
2936async function sendWelcomePanel() {
3037 try {
3138 const chatElement = document.getElementById('chat');
3239 if (!chatElement) {
3340 console.error('Chat element not found');
3441 return;
@@ -36,7 +43,7 @@ async function sendWelcomePanel() {
3643 const chats = await getRecentChats();
3744 const templateData = {
3845 chats,
3946 empty: !chats.length ,
4047 version: displayVersion,
4148 };
4249 const template = await renderTemplateAsync('welcomePanel', templateData);
@@ -51,7 +58,7 @@ async function sendWelcomePanel() {
5158 });
5259 });
5360 fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => {
5461 void newAssistantChat({ temporary: true });
5562 });
5663 chatElement.append(fragment.firstChild);
5764 } catch (error) {
@@ -114,7 +121,7 @@ async function getRecentChats() {
114121 /** @type {RecentChat[]} */
115122 const data = await response.json();
116123
117124 data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => {
118125 const character = characters.find(x => x.avatar === chat.avatar);
119126 if (!character) {
120127 console.warn(`Character not found for chat: ${chat.file_name}`);
@@ -133,6 +140,72 @@ async function getRecentChats() {
133140 return data;
134141}
135142
143+export async function openPermanentAssistantChat({ tryCreate = true } = {}) {
144+ const characterId = characters.findIndex(x => x.avatar === permanentAssistantAvatar);
145+ if (characterId === -1) {
146+ if (!tryCreate) {
147+ console.error(`Character not found for avatar ID: ${permanentAssistantAvatar}. Cannot create.`);
148+ return;
149+ }
150+
151+ try {
152+ console.log(`Character not found for avatar ID: ${permanentAssistantAvatar}. Creating new assistant.`);
153+ await createPermanentAssistant();
154+ return openPermanentAssistantChat({ tryCreate: false });
155+ }
156+ catch (error) {
157+ console.error('Error creating permanent assistant:', error);
158+ toastr.error(t`Failed to create ${neutralCharacterName}. See console for details.`);
159+ return;
160+ }
161+ }
162+
163+ try {
164+ await selectCharacterById(characterId);
165+ await doNewChat({ deleteCurrentChat: false });
166+ console.log(`Opened permanent assistant chat for ${neutralCharacterName}.`, getCurrentChatId());
167+ } catch (error) {
168+ console.error('Error opening permanent assistant chat:', error);
169+ toastr.error(t`Failed to open permanent assistant chat. See console for details.`);
170+ }
171+}
172+
173+async function createPermanentAssistant() {
174+ if (is_group_generating || is_send_press) {
175+ throw new Error(t`Cannot create while generating.`);
176+ }
177+
178+ const formData = new FormData();
179+ formData.append('ch_name', neutralCharacterName);
180+ formData.append('file_name', permanentAssistantAvatar.replace('.png', ''));
181+
182+ const headers = getRequestHeaders();
183+ delete headers['Content-Type'];
184+
185+ const fetchResult = await fetch('/api/characters/create', {
186+ method: 'POST',
187+ headers: headers,
188+ body: formData,
189+ cache: 'no-cache',
190+ });
191+
192+ if (!fetchResult.ok) {
193+ throw new Error(t`Creation request did not succeed.`);
194+ }
195+
196+ await getCharacters();
197+}
198+
199+export async function openPermanentAssistantCard() {
200+ const characterId = characters.findIndex(x => x.avatar === permanentAssistantAvatar);
201+ if (characterId === -1) {
202+ toastr.info(t`Assistant not found. Try sending a chat message.`);
203+ return;
204+ }
205+
206+ await selectCharacterById(characterId);
207+}
208+
136209export function initWelcomeScreen() {
137210 const events = [event_types.CHAT_CHANGED, event_types.APP_READY];
138211 for (const event of events) {
public/style.css+3 -3
@@ -6061,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized {
60616061 flex-wrap: nowrap;
60626062 justify-content: space-between;
60636063 align-items: center;
60646064 gap: 10px5px;
6065- padding: 0 2px;
60666065}
60676066
60686067.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) > div:not(.assistant_note_export) {
60696068 flex: 1;
6069+ text-align: left;
60706070}
60716071
60726072.oneline-dropdown label {
src/endpoints/characters.js+4 -4
@@ -577,10 +577,10 @@ function charaFormatData(data, directories) {
577577 _.set(char, 'mes_example', data.mes_example || '');
578578
579579 // Old ST extension fields (for backward compatibility, will be deprecated)
580580 _.set(char, 'creatorcomment', data.creator_notes || '');
581581 _.set(char, 'avatar', 'none');
582582 _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
583583 _.set(char, 'talkativeness', data.talkativeness || 0.5);
584584 _.set(char, 'fav', data.fav == 'true');
585585 _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []);
586586
@@ -604,7 +604,7 @@ function charaFormatData(data, directories) {
604604 _.set(char, 'data.alternate_greetings', getAlternateGreetings(data));
605605
606606 // ST extension fields to V2 object
607607 _.set(char, 'data.extensions.talkativeness', data.talkativeness || 0.5);
608608 _.set(char, 'data.extensions.fav', data.fav == 'true');
609609 _.set(char, 'data.extensions.world', data.world || '');
610610
@@ -1006,7 +1006,7 @@ router.post('/create', async function (request, response) {
10061006 request.body.ch_name = sanitize(request.body.ch_name);
10071007
10081008 const char = JSON.stringify(charaFormatData(request.body, request.user.directories));
10091009 const internalName = request.body.file_name || getPngName(request.body.ch_name, request.user.directories);
10101010 const avatarName = `${internalName}.png`;
10111011 const chatsPath = path.join(request.user.directories.chats, internalName);
10121012