move some functionality to a helper for testing

This commit is contained in:
Nate Kelley 2025-03-06 09:51:12 -07:00
parent 0728efe71a
commit db90ed13f4
No known key found for this signature in database
GPG Key ID: FD90372AB8D98B4F
3 changed files with 191 additions and 41 deletions

View File

@ -0,0 +1,121 @@
import { initializeOrUpdateMessage, updateChatTitle } from './chatStreamMessageHelper';
import { IBusterChatMessage, IBusterChat } from '../interfaces';
describe('initializeOrUpdateMessage', () => {
it('should initialize a new message when currentMessage is undefined', () => {
const messageId = 'test-id';
const updateFn = (draft: IBusterChatMessage) => {
if (draft.request_message) {
draft.request_message.request = 'test request';
}
};
const result = initializeOrUpdateMessage(messageId, undefined, updateFn);
expect(result.id).toBe(messageId);
expect(result.isCompletedStream).toBe(false);
expect(result.request_message?.request).toBe('test request');
expect(result.created_at).toBeDefined();
expect(result.final_reasoning_message).toBeNull();
});
it('should update an existing message', () => {
const messageId = 'test-id';
const currentMessage: IBusterChatMessage = {
id: messageId,
isCompletedStream: false,
request_message: {
request: 'original request',
sender_id: 'user1',
sender_name: 'Test User',
sender_avatar: null
},
response_message_ids: [],
reasoning_message_ids: [],
response_messages: {},
reasoning_messages: {},
created_at: new Date().toISOString(),
final_reasoning_message: null
};
const updateFn = (draft: IBusterChatMessage) => {
if (draft.request_message) {
draft.request_message.request = 'updated request';
}
};
const result = initializeOrUpdateMessage(messageId, currentMessage, updateFn);
expect(result.id).toBe(messageId);
expect(result.request_message?.request).toBe('updated request');
});
});
const mockChat: IBusterChat = {
id: 'test-chat-id',
title: 'Initial Title',
isNewChat: false,
is_favorited: false,
message_ids: [],
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
created_by: 'test-user',
created_by_id: 'test-user-id',
created_by_name: 'Test User',
created_by_avatar: null
};
describe('updateChatTitle', () => {
it('should update title with a chunk when progress is not completed', () => {
const event = {
chat_id: 'test-chat-id',
title: 'Final Title',
title_chunk: ' New',
progress: 'in_progress' as const
};
const result = updateChatTitle(mockChat, event);
expect(result.title).toBe('Initial Title New');
});
it('should set the final title when progress is completed', () => {
const event = {
chat_id: 'test-chat-id',
title: 'Final Title',
title_chunk: '',
progress: 'completed' as const
};
const result = updateChatTitle(mockChat, event);
expect(result.title).toBe('Final Title');
});
it('should handle chat with empty initial title', () => {
const chatWithoutTitle: IBusterChat = {
...mockChat,
title: ''
};
const event = {
chat_id: 'test-chat-id',
title: 'Final Title',
title_chunk: ' New',
progress: 'in_progress' as const
};
const result = updateChatTitle(chatWithoutTitle, event);
expect(result.title).toBe(' New');
});
it('should not modify title when title and title_chunk are empty', () => {
const event = {
chat_id: 'test-chat-id',
title: '',
title_chunk: '',
progress: 'completed' as const
};
const result = updateChatTitle(mockChat, event);
expect(result.title).toBe('Initial Title');
});
});

View File

@ -0,0 +1,43 @@
import { create } from 'mutative';
import { IBusterChat, IBusterChatMessage } from '../interfaces';
import { ChatEvent_GeneratingTitle } from '@/api/buster_socket/chats';
const createInitialMessage = (messageId: string): IBusterChatMessage => ({
id: messageId,
isCompletedStream: false,
request_message: {
request: '',
sender_id: '',
sender_name: '',
sender_avatar: null
},
response_message_ids: [],
reasoning_message_ids: [],
response_messages: {},
reasoning_messages: {},
created_at: new Date().toISOString(),
final_reasoning_message: null
});
export const initializeOrUpdateMessage = (
messageId: string,
currentMessage: IBusterChatMessage | undefined,
updateFn: (draft: IBusterChatMessage) => void
) => {
return create(currentMessage || createInitialMessage(messageId), (draft) => {
updateFn(draft);
});
};
export const updateChatTitle = (
currentChat: IBusterChat,
event: ChatEvent_GeneratingTitle
): IBusterChat => {
const { chat_id, title, title_chunk, progress } = event;
const isCompleted = progress === 'completed';
const currentTitle = currentChat.title || '';
const newTitle = isCompleted ? title : currentTitle + title_chunk;
return create(currentChat, (draft) => {
if (newTitle) draft.title = newTitle;
});
};

View File

@ -22,6 +22,7 @@ import { IBusterChat, IBusterChatMessage } from '../interfaces';
import { queryKeys } from '@/api/query_keys'; import { queryKeys } from '@/api/query_keys';
import { useQueryClient } from '@tanstack/react-query'; import { useQueryClient } from '@tanstack/react-query';
import { create } from 'mutative'; import { create } from 'mutative';
import { initializeOrUpdateMessage, updateChatTitle } from './chatStreamMessageHelper';
export const useChatStreamMessage = () => { export const useChatStreamMessage = () => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@ -50,17 +51,6 @@ export const useChatStreamMessage = () => {
} }
); );
const initializeOrUpdateMessage = useMemoizedFn(
(messageId: string, updateFn: (draft: IBusterChatMessage) => void) => {
const currentMessage = chatRefMessages.current[messageId];
const updatedMessage = create(currentMessage || {}, (draft) => {
updateFn(draft);
});
chatRefMessages.current[messageId] = updatedMessage;
onUpdateChatMessage(updatedMessage);
}
);
const normalizeChatMessage = useMemoizedFn( const normalizeChatMessage = useMemoizedFn(
(iChatMessages: Record<string, IBusterChatMessage>) => { (iChatMessages: Record<string, IBusterChatMessage>) => {
for (const message of Object.values(iChatMessages)) { for (const message of Object.values(iChatMessages)) {
@ -93,7 +83,6 @@ export const useChatStreamMessage = () => {
chatRef.current = create(chatRef.current, (draft) => { chatRef.current = create(chatRef.current, (draft) => {
draft[iChat.id] = iChat; draft[iChat.id] = iChat;
}); });
normalizeChatMessage(iChatMessages); normalizeChatMessage(iChatMessages);
onUpdateChat(iChat); onUpdateChat(iChat);
onChangePage({ onChangePage({
@ -119,32 +108,27 @@ export const useChatStreamMessage = () => {
); );
const _generatingTitleCallback = useMemoizedFn((_: null, newData: ChatEvent_GeneratingTitle) => { const _generatingTitleCallback = useMemoizedFn((_: null, newData: ChatEvent_GeneratingTitle) => {
const { chat_id, title, title_chunk, progress } = newData; const { chat_id } = newData;
const isCompleted = progress === 'completed'; const updatedChat = updateChatTitle(chatRef.current[chat_id], newData);
const currentTitle = chatRef.current[chat_id]?.title || ''; chatRef.current[chat_id] = updatedChat;
const newTitle = isCompleted ? title : currentTitle + title_chunk; onUpdateChat(updatedChat);
chatRef.current = create(chatRef.current, (draft) => {
if (newTitle) draft[chat_id].title = newTitle;
});
onUpdateChat({
id: chat_id,
title: newTitle
});
}); });
const _generatingResponseMessageCallback = useMemoizedFn( const _generatingResponseMessageCallback = useMemoizedFn(
(_: null, d: ChatEvent_GeneratingResponseMessage) => { (_: null, d: ChatEvent_GeneratingResponseMessage) => {
const { message_id, response_message, chat_id } = d; const { message_id, response_message } = d;
if (!response_message?.id) return; if (!response_message?.id) return;
const responseMessageId = response_message.id; const responseMessageId = response_message.id;
const existingMessage = const existingResponseMessage =
chatRefMessages.current[message_id]?.response_messages?.[responseMessageId]; chatRefMessages.current[message_id]?.response_messages?.[responseMessageId];
const isNewMessage = !existingMessage; const isNewResponseMessage = !existingResponseMessage;
if (isNewMessage) { let currentMessage = chatRefMessages.current[message_id];
initializeOrUpdateMessage(message_id, (draft) => {
if (isNewResponseMessage) {
currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
if (!draft.response_messages) { if (!draft.response_messages) {
draft.response_messages = {}; draft.response_messages = {};
} }
@ -157,11 +141,12 @@ export const useChatStreamMessage = () => {
} }
if (response_message.type === 'text') { if (response_message.type === 'text') {
const existingResponseMessageText = existingMessage as BusterChatResponseMessage_text; const existingResponseMessageText =
existingResponseMessage as BusterChatResponseMessage_text;
const isStreaming = const isStreaming =
response_message.message_chunk !== undefined && response_message.message_chunk !== null; response_message.message_chunk !== undefined && response_message.message_chunk !== null;
initializeOrUpdateMessage(message_id, (draft) => { currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
const responseMessage = draft.response_messages?.[responseMessageId]; const responseMessage = draft.response_messages?.[responseMessageId];
if (!responseMessage) return; if (!responseMessage) return;
const messageText = responseMessage as BusterChatMessageReasoning_text; const messageText = responseMessage as BusterChatMessageReasoning_text;
@ -176,7 +161,6 @@ export const useChatStreamMessage = () => {
}); });
} }
const currentMessage = chatRefMessages.current[message_id];
onUpdateChatMessageTransition({ onUpdateChatMessageTransition({
id: message_id, id: message_id,
response_messages: currentMessage?.response_messages, response_messages: currentMessage?.response_messages,
@ -190,12 +174,13 @@ export const useChatStreamMessage = () => {
const { message_id, reasoning, chat_id } = d; const { message_id, reasoning, chat_id } = d;
const reasoningMessageId = reasoning.id; const reasoningMessageId = reasoning.id;
const existingMessage = const existingReasoningMessage =
chatRefMessages.current[message_id]?.reasoning_messages?.[reasoningMessageId]; chatRefMessages.current[message_id]?.reasoning_messages?.[reasoningMessageId];
const isNewMessage = !existingMessage; const isNewReasoningMessage = !existingReasoningMessage;
let currentMessage = chatRefMessages.current[message_id];
if (isNewMessage) { if (isNewReasoningMessage) {
initializeOrUpdateMessage(message_id, (draft) => { currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
if (!draft.reasoning_messages) { if (!draft.reasoning_messages) {
draft.reasoning_messages = {}; draft.reasoning_messages = {};
} }
@ -209,11 +194,12 @@ export const useChatStreamMessage = () => {
switch (reasoning.type) { switch (reasoning.type) {
case 'text': { case 'text': {
const existingReasoningMessageText = existingMessage as BusterChatMessageReasoning_text; const existingReasoningMessageText =
existingReasoningMessage as BusterChatMessageReasoning_text;
const isStreaming = const isStreaming =
reasoning.message_chunk !== null || reasoning.message_chunk !== undefined; reasoning.message_chunk !== null || reasoning.message_chunk !== undefined;
initializeOrUpdateMessage(message_id, (draft) => { currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
const reasoningMessage = draft.reasoning_messages?.[reasoningMessageId]; const reasoningMessage = draft.reasoning_messages?.[reasoningMessageId];
if (!reasoningMessage) return; if (!reasoningMessage) return;
const messageText = reasoningMessage as BusterChatMessageReasoning_text; const messageText = reasoningMessage as BusterChatMessageReasoning_text;
@ -230,9 +216,10 @@ export const useChatStreamMessage = () => {
break; break;
} }
case 'files': { case 'files': {
const existingReasoningMessageFiles = existingMessage as BusterChatMessageReasoning_files; const existingReasoningMessageFiles =
existingReasoningMessage as BusterChatMessageReasoning_files;
initializeOrUpdateMessage(message_id, (draft) => { currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
const reasoningMessage = draft.reasoning_messages?.[reasoningMessageId]; const reasoningMessage = draft.reasoning_messages?.[reasoningMessageId];
if (!reasoningMessage) return; if (!reasoningMessage) return;
@ -286,7 +273,7 @@ export const useChatStreamMessage = () => {
break; break;
} }
case 'pills': { case 'pills': {
initializeOrUpdateMessage(message_id, (draft) => { currentMessage = initializeOrUpdateMessage(message_id, currentMessage, (draft) => {
if (!draft.reasoning_messages?.[reasoningMessageId]) return; if (!draft.reasoning_messages?.[reasoningMessageId]) return;
draft.reasoning_messages[reasoningMessageId] = reasoning; draft.reasoning_messages[reasoningMessageId] = reasoning;
}); });
@ -299,7 +286,6 @@ export const useChatStreamMessage = () => {
} }
} }
const currentMessage = chatRefMessages.current[message_id];
onUpdateChatMessageTransition({ onUpdateChatMessageTransition({
id: message_id, id: message_id,
reasoning_messages: currentMessage?.reasoning_messages, reasoning_messages: currentMessage?.reasoning_messages,