import { addOneMessage, characters, chat, displayVersion, doNewChat, event_types, eventSource, getCharacters, getCurrentChatId, getRequestHeaders, getSystemMessageByType, getThumbnailUrl, is_send_press, neutralCharacterName, newAssistantChat, openCharacterChat, printCharactersDebounced, selectCharacterById, system_avatar, system_message_types, this_chid, } from '../script.js'; import { getGroupAvatar, groups, is_group_generating, openGroupById, openGroupChat } from './group-chats.js'; import { t } from './i18n.js'; import { renderTemplateAsync } from './templates.js'; import { accountStorage } from './util/AccountStorage.js'; import { sortMoments, timestampToMoment } from './utils.js'; const assistantAvatarKey = 'assistant'; const defaultAssistantAvatar = 'default_Assistant.png'; const DEFAULT_DISPLAYED = 3; const MAX_DISPLAYED = 15; export function getPermanentAssistantAvatar() { const assistantAvatar = accountStorage.getItem(assistantAvatarKey); if (assistantAvatar === null) { return defaultAssistantAvatar; } const character = characters.find(x => x.avatar === assistantAvatar); if (character === undefined) { accountStorage.removeItem(assistantAvatarKey); return defaultAssistantAvatar; } return assistantAvatar; } export async function openWelcomeScreen() { const currentChatId = getCurrentChatId(); if (currentChatId !== undefined || chat.length > 0) { return; } const recentChats = await getRecentChats(); const chatAfterFetch = getCurrentChatId(); if (chatAfterFetch !== currentChatId) { console.debug('Chat changed while fetching recent chats.'); return; } await sendWelcomePanel(recentChats); sendAssistantMessage(); sendWelcomePrompt(); } function sendAssistantMessage() { const currentAssistantAvatar = getPermanentAssistantAvatar(); const character = characters.find(x => x.avatar === currentAssistantAvatar); const name = character ? character.name : neutralCharacterName; const avatar = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar; const message = { name: name, force_avatar: avatar, mes: t`If you're connected to an API, try asking me something!` + '\n***\n' + t`**Hint:** Set any character as your welcome page assistant from their "More..." menu.`, is_system: false, is_user: false, extra: { type: system_message_types.ASSISTANT_MESSAGE, }, }; chat.push(message); addOneMessage(message, { scroll: false }); } function sendWelcomePrompt() { const message = getSystemMessageByType(system_message_types.WELCOME_PROMPT); chat.push(message); addOneMessage(message, { scroll: false }); } /** * Sends the welcome panel to the chat. * @param {RecentChat[]} chats List of recent chats */ async function sendWelcomePanel(chats) { try { const chatElement = document.getElementById('chat'); const sendTextArea = document.getElementById('send_textarea'); if (!chatElement) { console.error('Chat element not found'); return; } const templateData = { chats, empty: !chats.length, version: displayVersion, more: chats.some(chat => chat.hidden), }; const template = await renderTemplateAsync('welcomePanel', templateData); const fragment = document.createRange().createContextualFragment(template); fragment.querySelectorAll('.welcomePanel').forEach((root) => { const recentHiddenClass = 'recentHidden'; const recentHiddenKey = 'WelcomePage_RecentChatsHidden'; if (accountStorage.getItem(recentHiddenKey) === 'true') { root.classList.add(recentHiddenClass); } root.querySelectorAll('.showRecentChats').forEach((button) => { button.addEventListener('click', () => { root.classList.remove(recentHiddenClass); accountStorage.setItem(recentHiddenKey, 'false'); }); }); root.querySelectorAll('.hideRecentChats').forEach((button) => { button.addEventListener('click', () => { root.classList.add(recentHiddenClass); accountStorage.setItem(recentHiddenKey, 'true'); }); }); }); fragment.querySelectorAll('.recentChat').forEach((item) => { item.addEventListener('click', () => { const avatarId = item.getAttribute('data-avatar'); const groupId = item.getAttribute('data-group'); const fileName = item.getAttribute('data-file'); if (avatarId && fileName) { void openRecentCharacterChat(avatarId, fileName); } if (groupId && fileName) { void openRecentGroupChat(groupId, fileName); } }); }); const hiddenChats = fragment.querySelectorAll('.recentChat.hidden'); fragment.querySelectorAll('button.showMoreChats').forEach((button) => { const showRecentChatsTitle = t`Show more recent chats`; const hideRecentChatsTitle = t`Show less recent chats`; button.setAttribute('title', showRecentChatsTitle); button.addEventListener('click', () => { const rotate = button.classList.contains('rotated'); hiddenChats.forEach((chatItem) => { chatItem.classList.toggle('hidden', rotate); }); button.classList.toggle('rotated', !rotate); button.setAttribute('title', rotate ? showRecentChatsTitle : hideRecentChatsTitle); }); }); fragment.querySelectorAll('button.openTemporaryChat').forEach((button) => { button.addEventListener('click', async () => { await newAssistantChat({ temporary: true }); if (sendTextArea instanceof HTMLTextAreaElement) { sendTextArea.focus(); } }); }); fragment.querySelectorAll('.recentChat.group').forEach((groupChat) => { const groupId = groupChat.getAttribute('data-group'); const group = groups.find(x => x.id === groupId); if (group) { const avatar = groupChat.querySelector('.avatar'); if (!avatar) { return; } const groupAvatar = getGroupAvatar(group); $(avatar).replaceWith(groupAvatar); } }); chatElement.append(fragment.firstChild); } catch (error) { console.error('Welcome screen error:', error); } } /** * Opens a recent character chat. * @param {string} avatarId Avatar file name * @param {string} fileName Chat file name */ async function openRecentCharacterChat(avatarId, fileName) { const characterId = characters.findIndex(x => x.avatar === avatarId); if (characterId === -1) { console.error(`Character not found for avatar ID: ${avatarId}`); return; } try { await selectCharacterById(characterId); const currentChatId = getCurrentChatId(); if (currentChatId === fileName) { console.debug(`Chat ${fileName} is already open.`); return; } await openCharacterChat(fileName); } catch (error) { console.error('Error opening recent chat:', error); toastr.error(t`Failed to open recent chat. See console for details.`); } } /** * Opens a recent group chat. * @param {string} groupId Group ID * @param {string} fileName Chat file name */ async function openRecentGroupChat(groupId, fileName) { const group = groups.find(x => x.id === groupId); if (!group) { console.error(`Group not found for ID: ${groupId}`); return; } try { await openGroupById(groupId); const currentChatId = getCurrentChatId(); if (currentChatId === fileName) { console.debug(`Chat ${fileName} is already open.`); return; } await openGroupChat(groupId, fileName); } catch (error) { console.error('Error opening recent group chat:', error); toastr.error(t`Failed to open recent group chat. See console for details.`); } } /** * Gets the list of recent chats from the server. * @returns {Promise} List of recent chats * * @typedef {object} RecentChat * @property {string} file_name Name of the chat file * @property {string} chat_name Name of the chat (without extension) * @property {string} file_size Size of the chat file * @property {number} chat_items Number of items in the chat * @property {string} mes Last message content * @property {string} last_mes Timestamp of the last message * @property {string} avatar Avatar URL * @property {string} char_thumbnail Thumbnail URL * @property {string} char_name Character or group name * @property {string} date_short Date in short format * @property {string} date_long Date in long format * @property {string} group Group ID (if applicable) * @property {boolean} is_group Indicates if the chat is a group chat * @property {boolean} hidden Chat will be hidden by default */ async function getRecentChats() { const response = await fetch('/api/chats/recent', { method: 'POST', headers: getRequestHeaders(), body: JSON.stringify({ max: MAX_DISPLAYED }), }); if (!response.ok) { console.warn('Failed to fetch recent character chats'); return []; } /** @type {RecentChat[]} */ const data = await response.json(); data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))) .map(chat => ({ chat, character: characters.find(x => x.avatar === chat.avatar), group: groups.find(x => x.id === chat.group) })) .filter(t => t.character || t.group) .forEach(({ chat, character, group }, index) => { const chatTimestamp = timestampToMoment(chat.last_mes); chat.char_name = character?.name || group?.name || ''; chat.date_short = chatTimestamp.format('l'); chat.date_long = chatTimestamp.format('LL LT'); chat.chat_name = chat.file_name.replace('.jsonl', ''); chat.char_thumbnail = character ? getThumbnailUrl('avatar', character.avatar) : system_avatar; chat.is_group = !!group; chat.hidden = index >= DEFAULT_DISPLAYED; chat.avatar = chat.avatar || ''; chat.group = chat.group || ''; }); return data; } export async function openPermanentAssistantChat({ tryCreate = true, created = false } = {}) { const avatar = getPermanentAssistantAvatar(); const characterId = characters.findIndex(x => x.avatar === avatar); if (characterId === -1) { if (!tryCreate) { console.error(`Character not found for avatar ID: ${avatar}. Cannot create.`); return; } try { console.log(`Character not found for avatar ID: ${avatar}. Creating new assistant.`); await createPermanentAssistant(); return openPermanentAssistantChat({ tryCreate: false, created: true }); } 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); if (!created) { 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', defaultAssistantAvatar.replace('.png', '')); formData.append('creator_notes', t`Automatically created character. Feel free to edit.`); try { const avatarResponse = await fetch(system_avatar); const avatarBlob = await avatarResponse.blob(); formData.append('avatar', avatarBlob, defaultAssistantAvatar); } catch (error) { console.warn('Error fetching system avatar. Fallback image will be used.', error); } 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 avatar = getPermanentAssistantAvatar(); const characterId = characters.findIndex(x => x.avatar === avatar); if (characterId === -1) { toastr.info(t`Assistant not found. Try sending a chat message.`); return; } await selectCharacterById(characterId); } /** * Assigns a character as the assistant. * @param {string?} characterId Character ID */ export function assignCharacterAsAssistant(characterId) { if (characterId === undefined) { return; } /** @type {import('./char-data.js').v1CharData} */ const character = characters[characterId]; if (!character) { return; } const currentAssistantAvatar = getPermanentAssistantAvatar(); if (currentAssistantAvatar === character.avatar) { if (character.avatar === defaultAssistantAvatar) { toastr.info(t`${character.name} is a system assistant. Choose another character.`); return; } toastr.info(t`${character.name} is no longer your assistant.`); accountStorage.removeItem(assistantAvatarKey); return; } accountStorage.setItem(assistantAvatarKey, character.avatar); printCharactersDebounced(); toastr.success(t`Set ${character.name} as your assistant.`); } export function initWelcomeScreen() { const events = [event_types.CHAT_CHANGED, event_types.APP_READY]; for (const event of events) { eventSource.makeFirst(event, openWelcomeScreen); } eventSource.on(event_types.CHARACTER_MANAGEMENT_DROPDOWN, (target) => { if (target !== 'set_as_assistant') { return; } assignCharacterAsAssistant(this_chid); }); eventSource.on(event_types.CHARACTER_RENAMED, (oldAvatar, newAvatar) => { if (oldAvatar === getPermanentAssistantAvatar()) { accountStorage.setItem(assistantAvatarKey, newAvatar); } }); }