optimize chat manager logic
| @@ -7025,99 +7025,15 @@ export async function displayPastChats() { | ||
| 7025 | 7025 | $('#select_chat_div').empty(); |
| 7026 | 7026 | $('#select_chat_search').val('').off('input'); |
| 7027 | 7027 | |
| 7028 | - const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats()); | |
| 7029 | - | |
| 7030 | - if (!data) { | |
| 7031 | - toastr.error(t`Could not load chat data. Try reloading the page.`); | |
| 7032 | - return; | |
| 7033 | - } | |
| 7034 | - | |
| 7035 | 7028 | const chatDetails = getCurrentChatDetails(); |
| 7036 | - const group = chatDetails.group; | |
| 7037 | 7029 | const currentChat = chatDetails.sessionName; |
| 7038 | 7030 | const displayName = chatDetails.characterName; |
| 7039 | 7031 | const avatarImg = chatDetails.avatarImgURL; |
| 7040 | 7032 | |
| 7041 | 7033 | constawait rawChatsdisplayChats('', =currentChat, awaitdisplayName, getChatsFromFiles(dataavatarImg, selected_group); |
| 7042 | - | |
| 7043 | - // Sort by last message date descending | |
| 7044 | - data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); | |
| 7045 | - console.log(data); | |
| 7046 | - $('#load_select_chat_div').css('display', 'none'); | |
| 7047 | - $('#ChatHistoryCharName').text(`${displayName}'s `); | |
| 7048 | - | |
| 7049 | - const displayChats = (searchQuery) => { | |
| 7050 | - $('#select_chat_div').empty(); // Clear the current chats before appending filtered chats | |
| 7051 | - | |
| 7052 | - const filteredData = data.filter(chat => { | |
| 7053 | - const fileName = chat['file_name']; | |
| 7054 | - const chatContent = rawChats[fileName]; | |
| 7055 | - | |
| 7056 | - // Make sure empty chats are displayed when there is no search query | |
| 7057 | - if (Array.isArray(chatContent) && !chatContent.length && !searchQuery) { | |
| 7058 | - return true; | |
| 7059 | - } | |
| 7060 | - | |
| 7061 | - // // Uncomment this to return to old behavior (classical full-substring search). | |
| 7062 | - // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase())); | |
| 7063 | - | |
| 7064 | - // Fragment search a.k.a. swoop (as in `helm-swoop` in the Helm package of Emacs). | |
| 7065 | - // Split a `query` {string} into its fragments {string[]}. | |
| 7066 | - function makeQueryFragments(query) { | |
| 7067 | - let fragments = query.trim().split(/\s+/).map(str => str.trim().toLowerCase()).filter(onlyUnique); | |
| 7068 | - // fragments = fragments.filter( function(str) { return str.length >= 3; } ); // Helm does this, but perhaps better if we don't. | |
| 7069 | - return fragments; | |
| 7070 | - } | |
| 7071 | - // Check whether `text` {string} includes all of the `fragments` {string[]}. | |
| 7072 | - function matchFragments(fragments, text) { | |
| 7073 | - if (!text || !text.toLowerCase) return false; | |
| 7074 | - return fragments.every(item => text.toLowerCase().includes(item)); | |
| 7075 | - } | |
| 7076 | - const fragments = makeQueryFragments(searchQuery); | |
| 7077 | - // At least one chat message must match *all* the fragments. | |
| 7078 | - // Currently, this doesn't match if the fragment matches are distributed across several chat messages. | |
| 7079 | - return chatContent && Object.values(chatContent).some(message => matchFragments(fragments, message?.mes)); | |
| 7080 | - }); | |
| 7081 | - | |
| 7082 | - console.debug(filteredData); | |
| 7083 | - for (const value of filteredData.values()) { | |
| 7084 | - let strlen = 300; | |
| 7085 | - let mes = value['mes']; | |
| 7086 | - | |
| 7087 | - if (mes !== undefined) { | |
| 7088 | - if (mes.length > strlen) { | |
| 7089 | - mes = '...' + mes.substring(mes.length - strlen); | |
| 7090 | - } | |
| 7091 | - const fileSize = value['file_size']; | |
| 7092 | - const fileName = value['file_name']; | |
| 7093 | - const chatItems = rawChats[fileName].length; | |
| 7094 | - const timestamp = timestampToMoment(value['last_mes']).format('lll'); | |
| 7095 | - const template = $('#past_chat_template .select_chat_block_wrapper').clone(); | |
| 7096 | - template.find('.select_chat_block').attr('file_name', fileName); | |
| 7097 | - template.find('.avatar img').attr('src', avatarImg); | |
| 7098 | - template.find('.select_chat_block_filename').text(fileName); | |
| 7099 | - template.find('.chat_file_size').text(`(${fileSize},`); | |
| 7100 | - template.find('.chat_messages_num').text(`${chatItems}💬)`); | |
| 7101 | - template.find('.select_chat_block_mes').text(mes); | |
| 7102 | - template.find('.PastChat_cross').attr('file_name', fileName); | |
| 7103 | - template.find('.chat_messages_date').text(timestamp); | |
| 7104 | - | |
| 7105 | - if (selected_group) { | |
| 7106 | - template.find('.avatar img').replaceWith(getGroupAvatar(group)); | |
| 7107 | - } | |
| 7108 | - | |
| 7109 | - $('#select_chat_div').append(template); | |
| 7110 | - | |
| 7111 | - if (currentChat === fileName.toString().replace('.jsonl', '')) { | |
| 7112 | - $('#select_chat_div').find('.select_chat_block:last').attr('highlight', String(true)); | |
| 7113 | - } | |
| 7114 | - } | |
| 7115 | - } | |
| 7116 | - }; | |
| 7117 | - displayChats(''); // Display all by default | |
| 7118 | 7034 | |
| 7119 | 7035 | const debouncedDisplay = debounce((searchQuery) => { |
| 7120 | 7036 | displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group); |
| 7121 | 7037 | }); |
| 7122 | 7038 | |
| 7123 | 7039 | // Define the search input listener |
| @@ -7135,6 +7051,48 @@ export async function displayPastChats() { | ||
| 7135 | 7051 | }, 200); |
| 7136 | 7052 | } |
| 7137 | 7053 | |
| 7054 | +async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group) { | |
| 7055 | + try { | |
| 7056 | + const response = await fetch('/api/chats/search', { | |
| 7057 | + method: 'POST', | |
| 7058 | + headers: getRequestHeaders(), | |
| 7059 | + body: JSON.stringify({ | |
| 7060 | + query: searchQuery, | |
| 7061 | + avatar_url: selected_group ? null : characters[this_chid].avatar, | |
| 7062 | + group_id: selected_group || null | |
| 7063 | + }), | |
| 7064 | + }); | |
| 7065 | + | |
| 7066 | + if (!response.ok) { | |
| 7067 | + throw new Error('Search failed'); | |
| 7068 | + } | |
| 7069 | + | |
| 7070 | + const filteredData = await response.json(); | |
| 7071 | + $('#select_chat_div').empty(); | |
| 7072 | + | |
| 7073 | + for (const chat of filteredData) { | |
| 7074 | + const template = $('#past_chat_template .select_chat_block_wrapper').clone(); | |
| 7075 | + template.find('.select_chat_block').attr('file_name', chat.file_name); | |
| 7076 | + template.find('.avatar img').attr('src', avatarImg); | |
| 7077 | + template.find('.select_chat_block_filename').text(chat.file_name); | |
| 7078 | + template.find('.chat_file_size').text(`(${chat.file_size},`); | |
| 7079 | + template.find('.chat_messages_num').text(`${chat.message_count}💬)`); | |
| 7080 | + template.find('.select_chat_block_mes').text(chat.preview_message); | |
| 7081 | + template.find('.PastChat_cross').attr('file_name', chat.file_name); | |
| 7082 | + template.find('.chat_messages_date').text(timestampToMoment(chat.last_mes).format('lll')); | |
| 7083 | + | |
| 7084 | + $('#select_chat_div').append(template); | |
| 7085 | + | |
| 7086 | + if (currentChat === chat.file_name) { | |
| 7087 | + $('#select_chat_div').find('.select_chat_block:last').attr('highlight', String(true)); | |
| 7088 | + } | |
| 7089 | + } | |
| 7090 | + } catch (error) { | |
| 7091 | + console.error('Error loading chats:', error); | |
| 7092 | + toastr.error('Could not load chat data. Try reloading the page.'); | |
| 7093 | + } | |
| 7094 | +} | |
| 7095 | + | |
| 7138 | 7096 | export function selectRightMenuWithAnimation(selectedMenuId) { |
| 7139 | 7097 | const displayModes = { |
| 7140 | 7098 | 'rm_group_chats_block': 'flex', |
| @@ -10421,6 +10379,11 @@ jQuery(async function () { | ||
| 10421 | 10379 | $('#load_select_chat_div').css('display', 'block'); |
| 10422 | 10380 | }); |
| 10423 | 10381 | |
| 10382 | + // not sure what that hourglass was for | |
| 10383 | + $('#option_select_chat').click(function () { | |
| 10384 | + $('#load_select_chat_div').css('display', 'none'); | |
| 10385 | + }); | |
| 10386 | + | |
| 10424 | 10387 | if (navigator.clipboard === undefined) { |
| 10425 | 10388 | // No clipboard support |
| 10426 | 10389 | $('.mes_copy').remove(); |
| @@ -52,6 +52,37 @@ function getBackupFunction(handle) { | ||
| 52 | 52 | return backupFunctions.get(handle); |
| 53 | 53 | } |
| 54 | 54 | |
| 55 | +/** | |
| 56 | + * Formats a byte size into a human-readable string with units | |
| 57 | + * @param {number} bytes - The size in bytes to format | |
| 58 | + * @returns {string} The formatted string (e.g., "1.5 MB") | |
| 59 | + */ | |
| 60 | +function formatBytes(bytes) { | |
| 61 | + if (bytes === 0) return '0 B'; | |
| 62 | + const k = 1024; | |
| 63 | + const sizes = ['B', 'KB', 'MB', 'GB']; | |
| 64 | + const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| 65 | + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| 66 | +} | |
| 67 | + | |
| 68 | +/** | |
| 69 | + * Gets a preview message from an array of chat messages | |
| 70 | + * @param {Array<Object>} messages - Array of chat messages, each with a 'mes' property | |
| 71 | + * @returns {string} A truncated preview of the last message or empty string if no messages | |
| 72 | + */ | |
| 73 | +function getPreviewMessage(messages) { | |
| 74 | + const strlen = 300; | |
| 75 | + const lastMessage = messages[messages.length - 1]?.mes; | |
| 76 | + | |
| 77 | + if (!lastMessage) { | |
| 78 | + return ''; | |
| 79 | + } | |
| 80 | + | |
| 81 | + return lastMessage.length > strlen | |
| 82 | + ? '...' + lastMessage.substring(lastMessage.length - strlen) | |
| 83 | + : lastMessage; | |
| 84 | +} | |
| 85 | + | |
| 55 | 86 | process.on('exit', () => { |
| 56 | 87 | for (const func of backupFunctions.values()) { |
| 57 | 88 | func.flush(); |
| @@ -516,3 +547,119 @@ router.post('/group/save', jsonParser, (request, response) => { | ||
| 516 | 547 | getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData); |
| 517 | 548 | return response.send({ ok: true }); |
| 518 | 549 | }); |
| 550 | + | |
| 551 | +router.post('/search', jsonParser, function (request, response) { | |
| 552 | + try { | |
| 553 | + const { query, avatar_url, group_id } = request.body; | |
| 554 | + let chatFiles = []; | |
| 555 | + | |
| 556 | + if (group_id) { | |
| 557 | + // Find group's chat IDs first | |
| 558 | + const groupDir = path.join(request.user.directories.groups); | |
| 559 | + const groupFiles = fs.readdirSync(groupDir) | |
| 560 | + .filter(file => file.endsWith('.json')); | |
| 561 | + | |
| 562 | + let targetGroup; | |
| 563 | + for (const groupFile of groupFiles) { | |
| 564 | + const groupData = JSON.parse(fs.readFileSync(path.join(groupDir, groupFile), 'utf8')); | |
| 565 | + if (groupData.id === group_id) { | |
| 566 | + targetGroup = groupData; | |
| 567 | + break; | |
| 568 | + } | |
| 569 | + } | |
| 570 | + | |
| 571 | + if (!targetGroup?.chats) { | |
| 572 | + return response.send([]); | |
| 573 | + } | |
| 574 | + | |
| 575 | + // Find group chat files for given group ID | |
| 576 | + const groupChatsDir = path.join(request.user.directories.groupChats); | |
| 577 | + chatFiles = targetGroup.chats | |
| 578 | + .map(chatId => { | |
| 579 | + const filePath = path.join(groupChatsDir, `${chatId}.jsonl`); | |
| 580 | + if (!fs.existsSync(filePath)) return null; | |
| 581 | + const stats = fs.statSync(filePath); | |
| 582 | + return { | |
| 583 | + file_name: chatId, | |
| 584 | + file_size: formatBytes(stats.size), | |
| 585 | + path: filePath, | |
| 586 | + }; | |
| 587 | + }) | |
| 588 | + .filter(x => x); | |
| 589 | + } else { | |
| 590 | + // Regular character chat directory | |
| 591 | + const character_name = avatar_url.replace('.png', ''); | |
| 592 | + const directoryPath = path.join(request.user.directories.chats, character_name); | |
| 593 | + | |
| 594 | + if (!fs.existsSync(directoryPath)) { | |
| 595 | + return response.send([]); | |
| 596 | + } | |
| 597 | + | |
| 598 | + chatFiles = fs.readdirSync(directoryPath) | |
| 599 | + .filter(file => file.endsWith('.jsonl')) | |
| 600 | + .map(fileName => { | |
| 601 | + const filePath = path.join(directoryPath, fileName); | |
| 602 | + const stats = fs.statSync(filePath); | |
| 603 | + return { | |
| 604 | + file_name: fileName, | |
| 605 | + file_size: formatBytes(stats.size), | |
| 606 | + path: filePath, | |
| 607 | + }; | |
| 608 | + }); | |
| 609 | + } | |
| 610 | + | |
| 611 | + const results = []; | |
| 612 | + | |
| 613 | + // Search logic | |
| 614 | + for (const chatFile of chatFiles) { | |
| 615 | + const data = fs.readFileSync(chatFile.path, 'utf8'); | |
| 616 | + const messages = data.split('\n') | |
| 617 | + .map(line => { try { return JSON.parse(line); } catch (_) { return null; } }) | |
| 618 | + .filter(x => x); | |
| 619 | + | |
| 620 | + if (messages.length === 0) { | |
| 621 | + continue; | |
| 622 | + } | |
| 623 | + | |
| 624 | + const lastMessage = messages[messages.length - 1]; | |
| 625 | + const lastMesDate = lastMessage?.send_date || new Date().toISOString(); | |
| 626 | + | |
| 627 | + // If no search query, just return metadata | |
| 628 | + if (!query) { | |
| 629 | + results.push({ | |
| 630 | + file_name: chatFile.file_name, | |
| 631 | + file_size: chatFile.file_size, | |
| 632 | + message_count: messages.length, | |
| 633 | + last_mes: lastMesDate, | |
| 634 | + preview_message: getPreviewMessage(messages), | |
| 635 | + }); | |
| 636 | + continue; | |
| 637 | + } | |
| 638 | + | |
| 639 | + // Search through messages | |
| 640 | + const fragments = query.trim().toLowerCase().split(/\s+/).filter(x => x); | |
| 641 | + const hasMatch = messages.some(message => { | |
| 642 | + const text = message?.mes?.toLowerCase(); | |
| 643 | + return text && fragments.every(fragment => text.includes(fragment)); | |
| 644 | + }); | |
| 645 | + | |
| 646 | + if (hasMatch) { | |
| 647 | + results.push({ | |
| 648 | + file_name: chatFile.file_name, | |
| 649 | + file_size: chatFile.file_size, | |
| 650 | + message_count: messages.length, | |
| 651 | + last_mes: lastMesDate, | |
| 652 | + preview_message: getPreviewMessage(messages), | |
| 653 | + }); | |
| 654 | + } | |
| 655 | + } | |
| 656 | + | |
| 657 | + // Sort by last message date descending | |
| 658 | + results.sort((a, b) => new Date(b.last_mes) - new Date(a.last_mes)); | |
| 659 | + return response.send(results); | |
| 660 | + | |
| 661 | + } catch (error) { | |
| 662 | + console.error('Chat search error:', error); | |
| 663 | + return response.status(500).json({ error: 'Search failed' }); | |
| 664 | + } | |
| 665 | +}); | |
| 665 | \ No newline at end of file | |