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';
282import { getContext } from './scripts/st-context.js';282import { getContext } from './scripts/st-context.js';
283import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';283import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js';
284import { accountStorage } from './scripts/util/AccountStorage.js';284import { accountStorage } from './scripts/util/AccountStorage.js';
285import { initWelcomeScreen } from './scripts/welcome-screen.js';285import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard } from './scripts/welcome-screen.js';
286286
287// API OBJECT FOR EXTERNAL WIRING287// API OBJECT FOR EXTERNAL WIRING
288globalThis.SillyTavern = {288globalThis.SillyTavern = {
@@ -2041,7 +2041,7 @@ export async function sendTextareaMessage() {
2041 }2041 }
20422042
2043 if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) {2043 if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) {
2044 await newAssistantChat();2044 await newAssistantChat({ temporary: false });
2045 }2045 }
20462046
2047 Generate(generateType);2047 Generate(generateType);
@@ -2883,7 +2883,14 @@ export async function processCommands(message) {
2883 return true;2883 return true;
2884}2884}
28852885
2886export 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 */
2893export function getSystemMessageByType(type, text, extra = {}) {
2887 const systemMessage = system_messages[type];2894 const systemMessage = system_messages[type];
28882895
2889 if (!systemMessage) {2896 if (!systemMessage) {
@@ -2906,7 +2913,17 @@ export function sendSystemMessage(type, text, extra = {}) {
29062913
2907 newMessage.extra = Object.assign(newMessage.extra, extra);2914 newMessage.extra = Object.assign(newMessage.extra, extra);
2908 newMessage.extra.type = type;2915 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 */
2925export function sendSystemMessage(type, text, extra = {}) {
2926 const newMessage = getSystemMessageByType(type, text, extra);
2910 chat.push(newMessage);2927 chat.push(newMessage);
2911 addOneMessage(newMessage);2928 addOneMessage(newMessage);
2912 is_send_press = false;2929 is_send_press = false;
@@ -10113,8 +10130,17 @@ async function removeCharacterFromUI() {
10113 saveSettingsDebounced();10130 saveSettingsDebounced();
10114}10131}
1011510132
10116export 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 */
10139export async function newAssistantChat({ temporary = false } = {}) {
10117 await clearChat();10140 await clearChat();
10141 if (!temporary) {
10142 return openPermanentAssistantChat();
10143 }
10118 chat.splice(0, chat.length);10144 chat.splice(0, chat.length);
10119 chat_metadata = {};10145 chat_metadata = {};
10120 setCharacterName(neutralCharacterName);10146 setCharacterName(neutralCharacterName);
@@ -10427,7 +10453,7 @@ jQuery(async function () {
10427 if (chatId) {10453 if (chatId) {
10428 return reject('Not in a temporary chat');10454 return reject('Not in a temporary chat');
10429 }10455 }
10430 await newAssistantChat();10456 await newAssistantChat({ temporary: true });
10431 return resolve('');10457 return resolve('');
10432 };10458 };
10433 eventSource.once(event_types.CHAT_CHANGED, eventCallback);10459 eventSource.once(event_types.CHAT_CHANGED, eventCallback);
@@ -11094,6 +11120,9 @@ jQuery(async function () {
11094 });11120 });
1109511121
11096 if (id == 'option_select_chat') {11122 if (id == 'option_select_chat') {
11123 if (this_chid === undefined && !is_send_press) {
11124 await openPermanentAssistantCard();
11125 }
11097 if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {11126 if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) {
11098 await displayPastChats();11127 await displayPastChats();
11099 //this is just to avoid the shadow for past chat view when using /delchat11128 //this is just to avoid the shadow for past chat view when using /delchat
@@ -11124,7 +11153,7 @@ jQuery(async function () {
11124 await doNewChat({ deleteCurrentChat: deleteCurrentChat });11153 await doNewChat({ deleteCurrentChat: deleteCurrentChat });
11125 }11154 }
11126 if (!selected_group && this_chid === undefined && !is_send_press) {11155 if (!selected_group && this_chid === undefined && !is_send_press) {
11127 await newAssistantChat();11156 await newAssistantChat({ temporary: true });
11128 }11157 }
11129 }11158 }
1113011159
public/scripts/chats.js+33 -0
@@ -23,6 +23,9 @@ import {
23 neutralCharacterName,23 neutralCharacterName,
24 updateChatMetadata,24 updateChatMetadata,
25 system_message_types,25 system_message_types,
26 getSystemMessageByType,
27 printMessages,
28 clearChat,
26} from '../script.js';29} from '../script.js';
27import { selected_group } from './group-chats.js';30import { selected_group } from './group-chats.js';
28import { power_user } from './power-user.js';31import { power_user } from './power-user.js';
@@ -37,6 +40,7 @@ import {
37 saveBase64AsFile,40 saveBase64AsFile,
38 extractTextFromOffice,41 extractTextFromOffice,
39 download,42 download,
43 getFileText,
40} from './utils.js';44} from './utils.js';
41import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';45import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js';
42import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';46import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js';
@@ -1497,6 +1501,35 @@ jQuery(function () {
1497 download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');1501 download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json');
1498 });1502 });
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
1500 // Do not change. #attachFile is added by extension.1533 // Do not change. #attachFile is added by extension.
1501 $(document).on('click', '#attachFile', function () {1534 $(document).on('click', '#attachFile', function () {
1502 $('#file_form_input').trigger('click');1535 $('#file_form_input').trigger('click');
public/scripts/templates/assistantNote.html+8 -4
@@ -1,9 +1,13 @@
1<div data-type="assistant_note">1<div data-type="assistant_note">
2 <div>2 <div class="assistant_note_title">
3 <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>3 <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>
5 </div>4 </div>
6 <div class="assistant_note_export menu_button menu_button_icon" data-i18n="[title]Export as JSONL" title="Export as JSONL">5 <button class="assistant_note_import menu_button menu_button_icon margin0" data-i18n="[title]Import from JSONL" title="Import from 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">
7 <i class="fa-solid fa-file-export"></i>10 <i class="fa-solid fa-file-export"></i>
8 </div>11 <span data-i18n="Save">Save</span>
12 </button>
9</div>13</div>
public/scripts/welcome-screen.js+77 -4
@@ -1,21 +1,28 @@
1import {1import {
2 characters,2 characters,
3 displayVersion,3 displayVersion,
4 doNewChat,
4 event_types,5 event_types,
5 eventSource,6 eventSource,
7 getCharacters,
6 getCurrentChatId,8 getCurrentChatId,
7 getRequestHeaders,9 getRequestHeaders,
8 getThumbnailUrl,10 getThumbnailUrl,
11 is_send_press,
12 neutralCharacterName,
9 newAssistantChat,13 newAssistantChat,
10 openCharacterChat,14 openCharacterChat,
11 selectCharacterById,15 selectCharacterById,
12 sendSystemMessage,16 sendSystemMessage,
13 system_message_types,17 system_message_types,
14} from '../script.js';18} from '../script.js';
19import { is_group_generating } from './group-chats.js';
15import { t } from './i18n.js';20import { t } from './i18n.js';
16import { renderTemplateAsync } from './templates.js';21import { renderTemplateAsync } from './templates.js';
17import { timestampToMoment } from './utils.js';22import { timestampToMoment } from './utils.js';
1823
24const permanentAssistantAvatar = 'default_Assistant.png';
25
19export async function openWelcomeScreen() {26export async function openWelcomeScreen() {
20 const currentChatId = getCurrentChatId();27 const currentChatId = getCurrentChatId();
21 if (currentChatId !== undefined) {28 if (currentChatId !== undefined) {
@@ -28,7 +35,7 @@ export async function openWelcomeScreen() {
2835
29async function sendWelcomePanel() {36async function sendWelcomePanel() {
30 try {37 try {
31 const chatElement = document.getElementById('chat');38 const chatElement = document.getElementById('chat');
32 if (!chatElement) {39 if (!chatElement) {
33 console.error('Chat element not found');40 console.error('Chat element not found');
34 return;41 return;
@@ -36,7 +43,7 @@ async function sendWelcomePanel() {
36 const chats = await getRecentChats();43 const chats = await getRecentChats();
37 const templateData = {44 const templateData = {
38 chats,45 chats,
39 empty: !chats.length ,46 empty: !chats.length,
40 version: displayVersion,47 version: displayVersion,
41 };48 };
42 const template = await renderTemplateAsync('welcomePanel', templateData);49 const template = await renderTemplateAsync('welcomePanel', templateData);
@@ -51,7 +58,7 @@ async function sendWelcomePanel() {
51 });58 });
52 });59 });
53 fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => {60 fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => {
54 void newAssistantChat();61 void newAssistantChat({ temporary: true });
55 });62 });
56 chatElement.append(fragment.firstChild);63 chatElement.append(fragment.firstChild);
57 } catch (error) {64 } catch (error) {
@@ -114,7 +121,7 @@ async function getRecentChats() {
114 /** @type {RecentChat[]} */121 /** @type {RecentChat[]} */
115 const data = await response.json();122 const data = await response.json();
116123
117 data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => {124 data.sort((a, b) => b.last_mes - a.last_mes).forEach((chat, index) => {
118 const character = characters.find(x => x.avatar === chat.avatar);125 const character = characters.find(x => x.avatar === chat.avatar);
119 if (!character) {126 if (!character) {
120 console.warn(`Character not found for chat: ${chat.file_name}`);127 console.warn(`Character not found for chat: ${chat.file_name}`);
@@ -133,6 +140,72 @@ async function getRecentChats() {
133 return data;140 return data;
134}141}
135142
143export 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
173async 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
199export 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
136export function initWelcomeScreen() {209export function initWelcomeScreen() {
137 const events = [event_types.CHAT_CHANGED, event_types.APP_READY];210 const events = [event_types.CHAT_CHANGED, event_types.APP_READY];
138 for (const event of events) {211 for (const event of events) {
public/style.css+3 -3
@@ -6061,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized {
6061 flex-wrap: nowrap;6061 flex-wrap: nowrap;
6062 justify-content: space-between;6062 justify-content: space-between;
6063 align-items: center;6063 align-items: center;
6064 gap: 10px;6064 gap: 5px;
6065 padding: 0 2px;
6066}6065}
60676066
6068.mes_text div[data-type="assistant_note"]:has(.assistant_note_export)>div:not(.assistant_note_export) {6067.mes_text div[data-type="assistant_note"]:has(.assistant_note_export) > div {
6069 flex: 1;6068 flex: 1;
6069 text-align: left;
6070}6070}
60716071
6072.oneline-dropdown label {6072.oneline-dropdown label {
src/endpoints/characters.js+4 -4
@@ -577,10 +577,10 @@ function charaFormatData(data, directories) {
577 _.set(char, 'mes_example', data.mes_example || '');577 _.set(char, 'mes_example', data.mes_example || '');
578578
579 // Old ST extension fields (for backward compatibility, will be deprecated)579 // Old ST extension fields (for backward compatibility, will be deprecated)
580 _.set(char, 'creatorcomment', data.creator_notes);580 _.set(char, 'creatorcomment', data.creator_notes || '');
581 _.set(char, 'avatar', 'none');581 _.set(char, 'avatar', 'none');
582 _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());582 _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime());
583 _.set(char, 'talkativeness', data.talkativeness);583 _.set(char, 'talkativeness', data.talkativeness || 0.5);
584 _.set(char, 'fav', data.fav == 'true');584 _.set(char, 'fav', data.fav == 'true');
585 _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []);585 _.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) {
604 _.set(char, 'data.alternate_greetings', getAlternateGreetings(data));604 _.set(char, 'data.alternate_greetings', getAlternateGreetings(data));
605605
606 // ST extension fields to V2 object606 // ST extension fields to V2 object
607 _.set(char, 'data.extensions.talkativeness', data.talkativeness);607 _.set(char, 'data.extensions.talkativeness', data.talkativeness || 0.5);
608 _.set(char, 'data.extensions.fav', data.fav == 'true');608 _.set(char, 'data.extensions.fav', data.fav == 'true');
609 _.set(char, 'data.extensions.world', data.world || '');609 _.set(char, 'data.extensions.world', data.world || '');
610610
@@ -1006,7 +1006,7 @@ router.post('/create', async function (request, response) {
1006 request.body.ch_name = sanitize(request.body.ch_name);1006 request.body.ch_name = sanitize(request.body.ch_name);
10071007
1008 const char = JSON.stringify(charaFormatData(request.body, request.user.directories));1008 const char = JSON.stringify(charaFormatData(request.body, request.user.directories));
1009 const internalName = getPngName(request.body.ch_name, request.user.directories);1009 const internalName = request.body.file_name || getPngName(request.body.ch_name, request.user.directories);
1010 const avatarName = `${internalName}.png`;1010 const avatarName = `${internalName}.png`;
1011 const chatsPath = path.join(request.user.directories.chats, internalName);1011 const chatsPath = path.join(request.user.directories.chats, internalName);
10121012