Merge pull request #3029 from P3il4/staging Optimize chat manager logic
Signed| @@ -5461,7 +5461,7 @@ | |||
| 5461 | </div> | 5461 | </div> |
| 5462 | <div id="selectChatPopupHeaderText" class="TxtLrgBoldCenter"> | 5462 | <div id="selectChatPopupHeaderText" class="TxtLrgBoldCenter"> |
| 5463 | <span id="ChatHistoryCharName"></span><span data-i18n="Chat History">Chat History</span> | 5463 | <span id="ChatHistoryCharName"></span><span data-i18n="Chat History">Chat History</span> |
| 5464 | <a href="https://docs.sillytavern.app/usage/core-concepts/chatfilemanagement/#chat-import" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a> | 5464 | <a href="https://docs.sillytavern.app/usage/core-concepts/chatfilemanagement/" class="notes-link" target="_blank"><span class="fa-solid fa-circle-question note-link-span"></span></a> |
| 5465 | </div> | 5465 | </div> |
| 5466 | <div id="newChatFromManageScreenButton" class="menu_button menu_button_icon"> | 5466 | <div id="newChatFromManageScreenButton" class="menu_button menu_button_icon"> |
| 5467 | <i class="fa-solid fa-plus"></i> | 5467 | <i class="fa-solid fa-plus"></i> |
| @@ -5471,13 +5471,10 @@ | |||
| 5471 | <i class="fa-solid fa-file-import"></i> | 5471 | <i class="fa-solid fa-file-import"></i> |
| 5472 | <span data-i18n="Import Chat">Import Chat</span> | 5472 | <span data-i18n="Import Chat">Import Chat</span> |
| 5473 | </div> | 5473 | </div> |
| 5474 | <input type="text" id="select_chat_search" class="text_pole flex1" data-i18n="[placeholder]Search..." placeholder="Search..." autocomplete="off"> | 5474 | <input type="search" id="select_chat_search" class="text_pole flex1" data-i18n="[placeholder]Search..." placeholder="Search..." autocomplete="off"> |
| 5475 | <div id="select_chat_cross" class="opacity50p hoverglow fa-solid fa-circle-xmark fontsize120p" alt="Close Past Chat Popup"></div> | 5475 | <div id="select_chat_cross" class="opacity50p hoverglow fa-solid fa-circle-xmark fontsize120p" alt="Close Past Chat Popup"></div> |
| 5476 | </div> | 5476 | </div> |
| 5477 | <div id="select_chat_div"></div> | 5477 | <div id="select_chat_div"></div> |
| 5478 | <div id="load_select_chat_div"> | ||
| 5479 | <div class="fa-solid fa-hourglass fa-spin"></div> | ||
| 5480 | </div> | ||
| 5481 | </div> | 5478 | </div> |
| 5482 | </div> | 5479 | </div> |
| 5483 | <div id="background_template" class="template_element"> | 5480 | <div id="background_template" class="template_element"> |
| @@ -5550,11 +5547,11 @@ | |||
| 5550 | <div class="select_chat_block_wrapper flex-container"> | 5547 | <div class="select_chat_block_wrapper flex-container"> |
| 5551 | <div class="select_chat_block wide100p flex-container" file_name=""> | 5548 | <div class="select_chat_block wide100p flex-container" file_name=""> |
| 5552 | <div id="select_chat_name_wrapper" class="flex-container alignitemscenter justifySpaceBetween wide100p"> | 5549 | <div id="select_chat_name_wrapper" class="flex-container alignitemscenter justifySpaceBetween wide100p"> |
| 5553 | <div> | 5550 | <div class="flex-container alignItemsCenter"> |
| 5554 | <small class="select_chat_block_filename select_chat_block_filename_item"></small> | 5551 | <small class="select_chat_block_filename select_chat_block_filename_item"></small> |
| 5555 | <div title="Rename chat file" class="renameChatButton hoverglow opacity50p fa-solid fa-pencil" data-i18n="[title]Rename chat file"></div> | 5552 | <div title="Rename chat file" class="renameChatButton hoverglow opacity50p fa-solid fa-pencil fa-sm" data-i18n="[title]Rename chat file"></div> |
| 5556 | </div> | 5553 | </div> |
| 5557 | <div class="flex-container gap10px"> | 5554 | <div class="flex-container gap10px alignItemsCenter"> |
| 5558 | <div class="select_chat_info flex-container"> | 5555 | <div class="select_chat_info flex-container"> |
| 5559 | <small class="chat_messages_date select_chat_block_filename_item"></small> | 5556 | <small class="chat_messages_date select_chat_block_filename_item"></small> |
| 5560 | <small class="chat_file_size select_chat_block_filename_item"></small> | 5557 | <small class="chat_file_size select_chat_block_filename_item"></small> |
| @@ -7026,99 +7026,15 @@ export async function displayPastChats() { | |||
| 7026 | $('#select_chat_div').empty(); | 7026 | $('#select_chat_div').empty(); |
| 7027 | $('#select_chat_search').val('').off('input'); | 7027 | $('#select_chat_search').val('').off('input'); |
| 7028 | 7028 | ||
| 7029 | const data = await (selected_group ? getGroupPastChats(selected_group) : getPastCharacterChats()); | ||
| 7030 | |||
| 7031 | if (!data) { | ||
| 7032 | toastr.error(t`Could not load chat data. Try reloading the page.`); | ||
| 7033 | return; | ||
| 7034 | } | ||
| 7035 | |||
| 7036 | const chatDetails = getCurrentChatDetails(); | 7029 | const chatDetails = getCurrentChatDetails(); |
| 7037 | const group = chatDetails.group; | ||
| 7038 | const currentChat = chatDetails.sessionName; | 7030 | const currentChat = chatDetails.sessionName; |
| 7039 | const displayName = chatDetails.characterName; | 7031 | const displayName = chatDetails.characterName; |
| 7040 | const avatarImg = chatDetails.avatarImgURL; | 7032 | const avatarImg = chatDetails.avatarImgURL; |
| 7041 | 7033 | ||
| 7042 | const rawChats = await getChatsFromFiles(data, selected_group); | 7034 | await displayChats('', currentChat, displayName, avatarImg, selected_group); |
| 7043 | |||
| 7044 | // Sort by last message date descending | ||
| 7045 | data.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); | ||
| 7046 | console.log(data); | ||
| 7047 | $('#load_select_chat_div').css('display', 'none'); | ||
| 7048 | $('#ChatHistoryCharName').text(`${displayName}'s `); | ||
| 7049 | |||
| 7050 | const displayChats = (searchQuery) => { | ||
| 7051 | $('#select_chat_div').empty(); // Clear the current chats before appending filtered chats | ||
| 7052 | |||
| 7053 | const filteredData = data.filter(chat => { | ||
| 7054 | const fileName = chat['file_name']; | ||
| 7055 | const chatContent = rawChats[fileName]; | ||
| 7056 | |||
| 7057 | // Make sure empty chats are displayed when there is no search query | ||
| 7058 | if (Array.isArray(chatContent) && !chatContent.length && !searchQuery) { | ||
| 7059 | return true; | ||
| 7060 | } | ||
| 7061 | |||
| 7062 | // // Uncomment this to return to old behavior (classical full-substring search). | ||
| 7063 | // return chatContent && Object.values(chatContent).some(message => message?.mes?.toLowerCase()?.includes(searchQuery.toLowerCase())); | ||
| 7064 | |||
| 7065 | // Fragment search a.k.a. swoop (as in `helm-swoop` in the Helm package of Emacs). | ||
| 7066 | // Split a `query` {string} into its fragments {string[]}. | ||
| 7067 | function makeQueryFragments(query) { | ||
| 7068 | let fragments = query.trim().split(/\s+/).map(str => str.trim().toLowerCase()).filter(onlyUnique); | ||
| 7069 | // fragments = fragments.filter( function(str) { return str.length >= 3; } ); // Helm does this, but perhaps better if we don't. | ||
| 7070 | return fragments; | ||
| 7071 | } | ||
| 7072 | // Check whether `text` {string} includes all of the `fragments` {string[]}. | ||
| 7073 | function matchFragments(fragments, text) { | ||
| 7074 | if (!text || !text.toLowerCase) return false; | ||
| 7075 | return fragments.every(item => text.toLowerCase().includes(item)); | ||
| 7076 | } | ||
| 7077 | const fragments = makeQueryFragments(searchQuery); | ||
| 7078 | // At least one chat message must match *all* the fragments. | ||
| 7079 | // Currently, this doesn't match if the fragment matches are distributed across several chat messages. | ||
| 7080 | return chatContent && Object.values(chatContent).some(message => matchFragments(fragments, message?.mes)); | ||
| 7081 | }); | ||
| 7082 | |||
| 7083 | console.debug(filteredData); | ||
| 7084 | for (const value of filteredData.values()) { | ||
| 7085 | let strlen = 300; | ||
| 7086 | let mes = value['mes']; | ||
| 7087 | |||
| 7088 | if (mes !== undefined) { | ||
| 7089 | if (mes.length > strlen) { | ||
| 7090 | mes = '...' + mes.substring(mes.length - strlen); | ||
| 7091 | } | ||
| 7092 | const fileSize = value['file_size']; | ||
| 7093 | const fileName = value['file_name']; | ||
| 7094 | const chatItems = rawChats[fileName].length; | ||
| 7095 | const timestamp = timestampToMoment(value['last_mes']).format('lll'); | ||
| 7096 | const template = $('#past_chat_template .select_chat_block_wrapper').clone(); | ||
| 7097 | template.find('.select_chat_block').attr('file_name', fileName); | ||
| 7098 | template.find('.avatar img').attr('src', avatarImg); | ||
| 7099 | template.find('.select_chat_block_filename').text(fileName); | ||
| 7100 | template.find('.chat_file_size').text(`(${fileSize},`); | ||
| 7101 | template.find('.chat_messages_num').text(`${chatItems}💬)`); | ||
| 7102 | template.find('.select_chat_block_mes').text(mes); | ||
| 7103 | template.find('.PastChat_cross').attr('file_name', fileName); | ||
| 7104 | template.find('.chat_messages_date').text(timestamp); | ||
| 7105 | |||
| 7106 | if (selected_group) { | ||
| 7107 | template.find('.avatar img').replaceWith(getGroupAvatar(group)); | ||
| 7108 | } | ||
| 7109 | |||
| 7110 | $('#select_chat_div').append(template); | ||
| 7111 | |||
| 7112 | if (currentChat === fileName.toString().replace('.jsonl', '')) { | ||
| 7113 | $('#select_chat_div').find('.select_chat_block:last').attr('highlight', String(true)); | ||
| 7114 | } | ||
| 7115 | } | ||
| 7116 | } | ||
| 7117 | }; | ||
| 7118 | displayChats(''); // Display all by default | ||
| 7119 | 7035 | ||
| 7120 | const debouncedDisplay = debounce((searchQuery) => { | 7036 | const debouncedDisplay = debounce((searchQuery) => { |
| 7121 | displayChats(searchQuery); | 7037 | displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group); |
| 7122 | }); | 7038 | }); |
| 7123 | 7039 | ||
| 7124 | // Define the search input listener | 7040 | // Define the search input listener |
| @@ -7136,6 +7052,53 @@ export async function displayPastChats() { | |||
| 7136 | }, 200); | 7052 | }, 200); |
| 7137 | } | 7053 | } |
| 7138 | 7054 | ||
| 7055 | async function displayChats(searchQuery, currentChat, displayName, avatarImg, selected_group) { | ||
| 7056 | try { | ||
| 7057 | const trimExtension = (fileName) => String(fileName).replace('.jsonl', ''); | ||
| 7058 | |||
| 7059 | const response = await fetch('/api/chats/search', { | ||
| 7060 | method: 'POST', | ||
| 7061 | headers: getRequestHeaders(), | ||
| 7062 | body: JSON.stringify({ | ||
| 7063 | query: searchQuery, | ||
| 7064 | avatar_url: selected_group ? null : characters[this_chid].avatar, | ||
| 7065 | group_id: selected_group || null, | ||
| 7066 | }), | ||
| 7067 | }); | ||
| 7068 | |||
| 7069 | if (!response.ok) { | ||
| 7070 | throw new Error('Search failed'); | ||
| 7071 | } | ||
| 7072 | |||
| 7073 | const filteredData = await response.json(); | ||
| 7074 | $('#select_chat_div').empty(); | ||
| 7075 | |||
| 7076 | filteredData.sort((a, b) => sortMoments(timestampToMoment(a.last_mes), timestampToMoment(b.last_mes))); | ||
| 7077 | |||
| 7078 | for (const chat of filteredData) { | ||
| 7079 | const isSelected = trimExtension(currentChat) === trimExtension(chat.file_name); | ||
| 7080 | const template = $('#past_chat_template .select_chat_block_wrapper').clone(); | ||
| 7081 | template.find('.select_chat_block').attr('file_name', chat.file_name); | ||
| 7082 | template.find('.avatar img').attr('src', avatarImg); | ||
| 7083 | template.find('.select_chat_block_filename').text(chat.file_name); | ||
| 7084 | template.find('.chat_file_size').text(`(${chat.file_size},`); | ||
| 7085 | template.find('.chat_messages_num').text(`${chat.message_count} 💬)`); | ||
| 7086 | template.find('.select_chat_block_mes').text(chat.preview_message); | ||
| 7087 | template.find('.PastChat_cross').attr('file_name', chat.file_name); | ||
| 7088 | template.find('.chat_messages_date').text(timestampToMoment(chat.last_mes).format('lll')); | ||
| 7089 | |||
| 7090 | if (isSelected) { | ||
| 7091 | template.find('.select_chat_block').attr('highlight', String(true)); | ||
| 7092 | } | ||
| 7093 | |||
| 7094 | $('#select_chat_div').append(template); | ||
| 7095 | } | ||
| 7096 | } catch (error) { | ||
| 7097 | console.error('Error loading chats:', error); | ||
| 7098 | toastr.error('Could not load chat data. Try reloading the page.'); | ||
| 7099 | } | ||
| 7100 | } | ||
| 7101 | |||
| 7139 | export function selectRightMenuWithAnimation(selectedMenuId) { | 7102 | export function selectRightMenuWithAnimation(selectedMenuId) { |
| 7140 | const displayModes = { | 7103 | const displayModes = { |
| 7141 | 'rm_group_chats_block': 'flex', | 7104 | 'rm_group_chats_block': 'flex', |
| @@ -10418,8 +10381,6 @@ jQuery(async function () { | |||
| 10418 | easing: animation_easing, | 10381 | easing: animation_easing, |
| 10419 | }); | 10382 | }); |
| 10420 | setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration); | 10383 | setTimeout(function () { $('#shadow_select_chat_popup').css('display', 'none'); }, animation_duration); |
| 10421 | //$("#shadow_select_chat_popup").css("display", "none"); | ||
| 10422 | $('#load_select_chat_div').css('display', 'block'); | ||
| 10423 | }); | 10384 | }); |
| 10424 | 10385 | ||
| 10425 | if (navigator.clipboard === undefined) { | 10386 | if (navigator.clipboard === undefined) { |
| @@ -10819,7 +10780,6 @@ jQuery(async function () { | |||
| 10819 | var formData = new FormData($('#form_import_chat').get(0)); | 10780 | var formData = new FormData($('#form_import_chat').get(0)); |
| 10820 | formData.append('user_name', name1); | 10781 | formData.append('user_name', name1); |
| 10821 | $('#select_chat_div').html(''); | 10782 | $('#select_chat_div').html(''); |
| 10822 | $('#load_select_chat_div').css('display', 'block'); | ||
| 10823 | 10783 | ||
| 10824 | if (selected_group) { | 10784 | if (selected_group) { |
| 10825 | await importGroupChat(formData); | 10785 | await importGroupChat(formData); |
| @@ -636,7 +636,6 @@ export function initBookmarks() { | |||
| 636 | } | 636 | } |
| 637 | 637 | ||
| 638 | $('#shadow_select_chat_popup').css('display', 'none'); | 638 | $('#shadow_select_chat_popup').css('display', 'none'); |
| 639 | $('#load_select_chat_div').css('display', 'block'); | ||
| 640 | }); | 639 | }); |
| 641 | 640 | ||
| 642 | $(document).on('click', '.mes_create_bookmark', async function () { | 641 | $(document).on('click', '.mes_create_bookmark', async function () { |
| @@ -4244,8 +4244,8 @@ h5 { | |||
| 4244 | } | 4244 | } |
| 4245 | 4245 | ||
| 4246 | #select_chat_popup { | 4246 | #select_chat_popup { |
| 4247 | display: grid; | 4247 | display: flex; |
| 4248 | grid-template-rows: auto auto; | 4248 | flex-direction: column; |
| 4249 | max-width: var(--sheldWidth); | 4249 | max-width: var(--sheldWidth); |
| 4250 | height: min-content; | 4250 | height: min-content; |
| 4251 | max-height: calc(100vh - var(--topBarBlockSize)); | 4251 | max-height: calc(100vh - var(--topBarBlockSize)); |
| @@ -4265,7 +4265,7 @@ h5 { | |||
| 4265 | padding: 10px; | 4265 | padding: 10px; |
| 4266 | background-color: var(--SmartThemeBlurTintColor); | 4266 | background-color: var(--SmartThemeBlurTintColor); |
| 4267 | border-radius: 10px; | 4267 | border-radius: 10px; |
| 4268 | overflow-y: auto; | 4268 | overflow-y: hidden; |
| 4269 | border: 1px solid var(--SmartThemeBorderColor); | 4269 | border: 1px solid var(--SmartThemeBorderColor); |
| 4270 | } | 4270 | } |
| 4271 | 4271 | ||
| @@ -4273,20 +4273,10 @@ h5 { | |||
| 4273 | cursor: pointer; | 4273 | cursor: pointer; |
| 4274 | } | 4274 | } |
| 4275 | 4275 | ||
| 4276 | #load_select_chat_div { | ||
| 4277 | position: absolute; | ||
| 4278 | bottom: 154px; | ||
| 4279 | left: 174px; | ||
| 4280 | } | ||
| 4281 | |||
| 4282 | #load_select_chat_div img { | ||
| 4283 | width: 80px; | ||
| 4284 | height: 80px; | ||
| 4285 | } | ||
| 4286 | |||
| 4287 | #select_chat_div { | 4276 | #select_chat_div { |
| 4288 | padding: 0; | 4277 | padding: 0; |
| 4289 | height: min-content; | 4278 | height: 100%; |
| 4279 | overflow-y: auto; | ||
| 4290 | } | 4280 | } |
| 4291 | 4281 | ||
| 4292 | #select_chat_div hr { | 4282 | #select_chat_div hr { |
| @@ -4333,6 +4323,11 @@ h5 { | |||
| 4333 | 4323 | ||
| 4334 | .select_chat_block_mes { | 4324 | .select_chat_block_mes { |
| 4335 | font-size: calc(var(--mainFontSize) - .25rem); | 4325 | font-size: calc(var(--mainFontSize) - .25rem); |
| 4326 | display: -webkit-box; | ||
| 4327 | -webkit-line-clamp: 3; | ||
| 4328 | -webkit-box-orient: vertical; | ||
| 4329 | line-clamp: 3; | ||
| 4330 | overflow: hidden; | ||
| 4336 | } | 4331 | } |
| 4337 | 4332 | ||
| 4338 | .PastChat_cross { | 4333 | .PastChat_cross { |
| @@ -52,6 +52,37 @@ function getBackupFunction(handle) { | |||
| 52 | return backupFunctions.get(handle); | 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 = 400; | ||
| 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 | process.on('exit', () => { | 86 | process.on('exit', () => { |
| 56 | for (const func of backupFunctions.values()) { | 87 | for (const func of backupFunctions.values()) { |
| 57 | func.flush(); | 88 | func.flush(); |
| @@ -516,3 +547,119 @@ router.post('/group/save', jsonParser, (request, response) => { | |||
| 516 | getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData); | 547 | getBackupFunction(request.user.profile.handle)(request.user.directories.backups, String(id), jsonlData); |
| 517 | return response.send({ ok: true }); | 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 && typeof x.mes === 'string'); | ||
| 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 | }); | ||