Permanent assistant autocreation and temporary chat restore
| @@ -282,7 +282,7 @@ import { deriveTemplatesFromChatTemplate } from './scripts/chat-templates.js'; | ||
| 282 | 282 | import { getContext } from './scripts/st-context.js'; |
| 283 | 283 | import { extractReasoningFromData, initReasoning, parseReasoningInSwipes, PromptReasoning, ReasoningHandler, removeReasoningFromString, updateReasoningUI } from './scripts/reasoning.js'; |
| 284 | 284 | import { accountStorage } from './scripts/util/AccountStorage.js'; |
| 285 | 285 | import { initWelcomeScreen, openPermanentAssistantChat, openPermanentAssistantCard } from './scripts/welcome-screen.js'; |
| 286 | 286 | |
| 287 | 287 | // API OBJECT FOR EXTERNAL WIRING |
| 288 | 288 | globalThis.SillyTavern = { |
| @@ -2041,7 +2041,7 @@ export async function sendTextareaMessage() { | ||
| 2041 | 2041 | } |
| 2042 | 2042 | |
| 2043 | 2043 | if (textareaText && !selected_group && this_chid === undefined && name2 !== neutralCharacterName) { |
| 2044 | 2044 | await newAssistantChat({ temporary: false }); |
| 2045 | 2045 | } |
| 2046 | 2046 | |
| 2047 | 2047 | Generate(generateType); |
| @@ -2883,7 +2883,14 @@ export async function processCommands(message) { | ||
| 2883 | 2883 | return true; |
| 2884 | 2884 | } |
| 2885 | 2885 | |
| 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 = {}) { | |
| 2887 | 2894 | const systemMessage = system_messages[type]; |
| 2888 | 2895 | |
| 2889 | 2896 | if (!systemMessage) { |
| @@ -2906,7 +2913,17 @@ export function sendSystemMessage(type, text, extra = {}) { | ||
| 2906 | 2913 | |
| 2907 | 2914 | newMessage.extra = Object.assign(newMessage.extra, extra); |
| 2908 | 2915 | newMessage.extra.type = type; |
| 2916 | + return newMessage; | |
| 2917 | +} | |
| 2909 | 2918 | |
| 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); | |
| 2910 | 2927 | chat.push(newMessage); |
| 2911 | 2928 | addOneMessage(newMessage); |
| 2912 | 2929 | is_send_press = false; |
| @@ -10113,8 +10130,17 @@ async function removeCharacterFromUI() { | ||
| 10113 | 10130 | saveSettingsDebounced(); |
| 10114 | 10131 | } |
| 10115 | 10132 | |
| 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 } = {}) { | |
| 10117 | 10140 | await clearChat(); |
| 10141 | + if (!temporary) { | |
| 10142 | + return openPermanentAssistantChat(); | |
| 10143 | + } | |
| 10118 | 10144 | chat.splice(0, chat.length); |
| 10119 | 10145 | chat_metadata = {}; |
| 10120 | 10146 | setCharacterName(neutralCharacterName); |
| @@ -10427,7 +10453,7 @@ jQuery(async function () { | ||
| 10427 | 10453 | if (chatId) { |
| 10428 | 10454 | return reject('Not in a temporary chat'); |
| 10429 | 10455 | } |
| 10430 | 10456 | await newAssistantChat({ temporary: true }); |
| 10431 | 10457 | return resolve(''); |
| 10432 | 10458 | }; |
| 10433 | 10459 | eventSource.once(event_types.CHAT_CHANGED, eventCallback); |
| @@ -11094,6 +11120,9 @@ jQuery(async function () { | ||
| 11094 | 11120 | }); |
| 11095 | 11121 | |
| 11096 | 11122 | if (id == 'option_select_chat') { |
| 11123 | + if (this_chid === undefined && !is_send_press) { | |
| 11124 | + await openPermanentAssistantCard(); | |
| 11125 | + } | |
| 11097 | 11126 | if ((selected_group && !is_group_generating) || (this_chid !== undefined && !is_send_press) || fromSlashCommand) { |
| 11098 | 11127 | await displayPastChats(); |
| 11099 | 11128 | //this is just to avoid the shadow for past chat view when using /delchat |
| @@ -11124,7 +11153,7 @@ jQuery(async function () { | ||
| 11124 | 11153 | await doNewChat({ deleteCurrentChat: deleteCurrentChat }); |
| 11125 | 11154 | } |
| 11126 | 11155 | if (!selected_group && this_chid === undefined && !is_send_press) { |
| 11127 | 11156 | await newAssistantChat({ temporary: true }); |
| 11128 | 11157 | } |
| 11129 | 11158 | } |
| 11130 | 11159 | |
| @@ -23,6 +23,9 @@ import { | ||
| 23 | 23 | neutralCharacterName, |
| 24 | 24 | updateChatMetadata, |
| 25 | 25 | system_message_types, |
| 26 | + getSystemMessageByType, | |
| 27 | + printMessages, | |
| 28 | + clearChat, | |
| 26 | 29 | } from '../script.js'; |
| 27 | 30 | import { selected_group } from './group-chats.js'; |
| 28 | 31 | import { power_user } from './power-user.js'; |
| @@ -37,6 +40,7 @@ import { | ||
| 37 | 40 | saveBase64AsFile, |
| 38 | 41 | extractTextFromOffice, |
| 39 | 42 | download, |
| 43 | + getFileText, | |
| 40 | 44 | } from './utils.js'; |
| 41 | 45 | import { extension_settings, renderExtensionTemplateAsync, saveMetadataDebounced } from './extensions.js'; |
| 42 | 46 | import { POPUP_RESULT, POPUP_TYPE, Popup, callGenericPopup } from './popup.js'; |
| @@ -1497,6 +1501,35 @@ jQuery(function () { | ||
| 1497 | 1501 | download(chatToSave.map((m) => JSON.stringify(m)).join('\n'), `Assistant - ${humanizedDateTime()}.jsonl`, 'application/json'); |
| 1498 | 1502 | }); |
| 1499 | 1503 | |
| 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 | 1533 | // Do not change. #attachFile is added by extension. |
| 1501 | 1534 | $(document).on('click', '#attachFile', function () { |
| 1502 | 1535 | $('#file_form_input').trigger('click'); |
| @@ -1,9 +1,13 @@ | ||
| 1 | 1 | <div data-type="assistant_note"> |
| 2 | 2 | <div class="assistant_note_title"> |
| 3 | 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 | 4 | </div> |
| 6 | 5 | <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"> | |
| 7 | 10 | <i class="fa-solid fa-file-export"></i> |
| 8 | - </div> | |
| 11 | + <span data-i18n="Save">Save</span> | |
| 12 | + </button> | |
| 9 | 13 | </div> |
| @@ -1,21 +1,28 @@ | ||
| 1 | 1 | import { |
| 2 | 2 | characters, |
| 3 | 3 | displayVersion, |
| 4 | + doNewChat, | |
| 4 | 5 | event_types, |
| 5 | 6 | eventSource, |
| 7 | + getCharacters, | |
| 6 | 8 | getCurrentChatId, |
| 7 | 9 | getRequestHeaders, |
| 8 | 10 | getThumbnailUrl, |
| 11 | + is_send_press, | |
| 12 | + neutralCharacterName, | |
| 9 | 13 | newAssistantChat, |
| 10 | 14 | openCharacterChat, |
| 11 | 15 | selectCharacterById, |
| 12 | 16 | sendSystemMessage, |
| 13 | 17 | system_message_types, |
| 14 | 18 | } from '../script.js'; |
| 19 | +import { is_group_generating } from './group-chats.js'; | |
| 15 | 20 | import { t } from './i18n.js'; |
| 16 | 21 | import { renderTemplateAsync } from './templates.js'; |
| 17 | 22 | import { timestampToMoment } from './utils.js'; |
| 18 | 23 | |
| 24 | +const permanentAssistantAvatar = 'default_Assistant.png'; | |
| 25 | + | |
| 19 | 26 | export async function openWelcomeScreen() { |
| 20 | 27 | const currentChatId = getCurrentChatId(); |
| 21 | 28 | if (currentChatId !== undefined) { |
| @@ -51,7 +58,7 @@ async function sendWelcomePanel() { | ||
| 51 | 58 | }); |
| 52 | 59 | }); |
| 53 | 60 | fragment.querySelector('button.openTemporaryChat').addEventListener('click', () => { |
| 54 | 61 | void newAssistantChat({ temporary: true }); |
| 55 | 62 | }); |
| 56 | 63 | chatElement.append(fragment.firstChild); |
| 57 | 64 | } catch (error) { |
| @@ -133,6 +140,72 @@ async function getRecentChats() { | ||
| 133 | 140 | return data; |
| 134 | 141 | } |
| 135 | 142 | |
| 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 | + | |
| 136 | 209 | export function initWelcomeScreen() { |
| 137 | 210 | const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; |
| 138 | 211 | for (const event of events) { |
| @@ -6061,12 +6061,12 @@ body:not(.movingUI) .drawer-content.maximized { | ||
| 6061 | 6061 | flex-wrap: nowrap; |
| 6062 | 6062 | justify-content: space-between; |
| 6063 | 6063 | align-items: center; |
| 6064 | 6064 | gap: 10px5px; |
| 6065 | - padding: 0 2px; | |
| 6066 | 6065 | } |
| 6067 | 6066 | |
| 6068 | 6067 | .mes_text div[data-type="assistant_note"]:has(.assistant_note_export) > div:not(.assistant_note_export) { |
| 6069 | 6068 | flex: 1; |
| 6069 | + text-align: left; | |
| 6070 | 6070 | } |
| 6071 | 6071 | |
| 6072 | 6072 | .oneline-dropdown label { |
| @@ -577,10 +577,10 @@ function charaFormatData(data, directories) { | ||
| 577 | 577 | _.set(char, 'mes_example', data.mes_example || ''); |
| 578 | 578 | |
| 579 | 579 | // Old ST extension fields (for backward compatibility, will be deprecated) |
| 580 | 580 | _.set(char, 'creatorcomment', data.creator_notes || ''); |
| 581 | 581 | _.set(char, 'avatar', 'none'); |
| 582 | 582 | _.set(char, 'chat', data.ch_name + ' - ' + humanizedISO8601DateTime()); |
| 583 | 583 | _.set(char, 'talkativeness', data.talkativeness || 0.5); |
| 584 | 584 | _.set(char, 'fav', data.fav == 'true'); |
| 585 | 585 | _.set(char, 'tags', typeof data.tags == 'string' ? (data.tags.split(',').map(x => x.trim()).filter(x => x)) : data.tags || []); |
| 586 | 586 | |
| @@ -604,7 +604,7 @@ function charaFormatData(data, directories) { | ||
| 604 | 604 | _.set(char, 'data.alternate_greetings', getAlternateGreetings(data)); |
| 605 | 605 | |
| 606 | 606 | // ST extension fields to V2 object |
| 607 | 607 | _.set(char, 'data.extensions.talkativeness', data.talkativeness || 0.5); |
| 608 | 608 | _.set(char, 'data.extensions.fav', data.fav == 'true'); |
| 609 | 609 | _.set(char, 'data.extensions.world', data.world || ''); |
| 610 | 610 | |
| @@ -1006,7 +1006,7 @@ router.post('/create', async function (request, response) { | ||
| 1006 | 1006 | request.body.ch_name = sanitize(request.body.ch_name); |
| 1007 | 1007 | |
| 1008 | 1008 | const char = JSON.stringify(charaFormatData(request.body, request.user.directories)); |
| 1009 | 1009 | const internalName = request.body.file_name || getPngName(request.body.ch_name, request.user.directories); |
| 1010 | 1010 | const avatarName = `${internalName}.png`; |
| 1011 | 1011 | const chatsPath = path.join(request.user.directories.chats, internalName); |
| 1012 | 1012 | |