mirror of https://github.com/buster-so/buster.git
add streaming text
This commit is contained in:
parent
20c31af9da
commit
975b8dd8d9
|
@ -1,19 +1,21 @@
|
|||
import type { BusterChatMessage } from '@/api/buster_socket/chats';
|
||||
import React from 'react';
|
||||
import { ChatUserMessage } from './ChatUserMessage';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { ChatResponseMessages } from './ChatResponseMessages';
|
||||
import { createStyles } from 'antd-style';
|
||||
import type { IBusterChatMessage } from '@/context/Chats/interfaces';
|
||||
|
||||
export const ChatMessageBlock: React.FC<{ message: BusterChatMessage }> = React.memo(
|
||||
export const ChatMessageBlock: React.FC<{ message: IBusterChatMessage }> = React.memo(
|
||||
({ message }) => {
|
||||
const { styles, cx } = useStyles();
|
||||
|
||||
const { request_message, response_messages, id } = message;
|
||||
const { request_message, response_messages, id, isCompletedStream } = message;
|
||||
return (
|
||||
<div className={cx(styles.messageBlock, 'flex flex-col space-y-3.5 px-4 py-2')} id={id}>
|
||||
<ChatUserMessage requestMessage={request_message} />
|
||||
<ChatResponseMessages responseMessages={response_messages} />
|
||||
<ChatResponseMessages
|
||||
responseMessages={response_messages}
|
||||
isCompletedStream={isCompletedStream}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -0,0 +1,10 @@
|
|||
import React from 'react';
|
||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||
|
||||
export const ChatResponseMessage_File: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage }) => {
|
||||
return <div>ChatResponseMessage_File</div>;
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessage_File.displayName = 'ChatResponseMessage_File';
|
|
@ -0,0 +1,44 @@
|
|||
import { BusterChatMessage_text } from '@/api/buster_socket/chats';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { animationConfig } from './animationConfig';
|
||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||
|
||||
export const ChatResponseMessage_Text: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage: responseMessageProp, isCompletedStream }) => {
|
||||
const responseMessage = responseMessageProp as BusterChatMessage_text;
|
||||
|
||||
const [textChunks, setTextChunks] = useState<string[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
if (responseMessage.message_chunk && !responseMessage.message) {
|
||||
// Handle streaming chunks
|
||||
setTextChunks((prevChunks) => [...prevChunks, responseMessage.message_chunk]);
|
||||
} else if (responseMessage.message) {
|
||||
// Handle complete message
|
||||
const currentText = textChunks.join('');
|
||||
if (responseMessage.message.startsWith(currentText)) {
|
||||
const remainingText = responseMessage.message.slice(currentText.length);
|
||||
if (remainingText) {
|
||||
setTextChunks((prevChunks) => [...prevChunks, remainingText]);
|
||||
}
|
||||
} else {
|
||||
// If there's a mismatch, just use the complete message
|
||||
setTextChunks([responseMessage.message]);
|
||||
}
|
||||
}
|
||||
}, [responseMessage?.message_chunk, responseMessage?.message]);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{textChunks.map((chunk, index) => (
|
||||
<AnimatePresence key={index} initial={!isCompletedStream}>
|
||||
<motion.span {...animationConfig}>{chunk}</motion.span>
|
||||
</AnimatePresence>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessage_Text.displayName = 'ChatResponseMessage_Text';
|
|
@ -0,0 +1,11 @@
|
|||
import { BusterChatMessageResponse } from '@/api/buster_socket/chats';
|
||||
import React from 'react';
|
||||
import { ChatResponseMessageProps } from './ChatResponseMessages';
|
||||
|
||||
export const ChatResponseMessage_Thought: React.FC<ChatResponseMessageProps> = React.memo(
|
||||
({ responseMessage }) => {
|
||||
return <div>ChatResponseMessage_Thought</div>;
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessage_Thought.displayName = 'ChatResponseMessage_Thought';
|
|
@ -1,10 +1,88 @@
|
|||
import React from 'react';
|
||||
import type { BusterChatMessageResponse } from '@/api/buster_socket/chats';
|
||||
import React, { useEffect, useRef, useState } from 'react';
|
||||
import type { BusterChatMessage_text, BusterChatMessageResponse } from '@/api/buster_socket/chats';
|
||||
import { MessageContainer } from '../MessageContainer';
|
||||
import { ChatResponseMessage_File } from './ChatResponseMessage_File';
|
||||
import { ChatResponseMessage_Text } from './ChatResponseMessage_Text';
|
||||
import { ChatResponseMessage_Thought } from './ChatResponseMessage_Thought';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { faker } from '@faker-js/faker';
|
||||
|
||||
export const ChatResponseMessages: React.FC<{ responseMessages: BusterChatMessageResponse[] }> =
|
||||
React.memo(({ responseMessages }) => {
|
||||
return <MessageContainer senderName="">ChatResponseMessages</MessageContainer>;
|
||||
});
|
||||
export interface ChatResponseMessageProps {
|
||||
responseMessage: BusterChatMessageResponse;
|
||||
isCompletedStream: boolean;
|
||||
}
|
||||
|
||||
const ChatResponseMessageRecord: Record<
|
||||
BusterChatMessageResponse['type'],
|
||||
React.FC<ChatResponseMessageProps>
|
||||
> = {
|
||||
text: ChatResponseMessage_Text,
|
||||
file: ChatResponseMessage_File,
|
||||
thought: ChatResponseMessage_Thought
|
||||
};
|
||||
|
||||
interface ChatResponseMessagesProps {
|
||||
responseMessages: BusterChatMessageResponse[];
|
||||
isCompletedStream: boolean;
|
||||
}
|
||||
|
||||
export const ChatResponseMessages: React.FC<ChatResponseMessagesProps> = React.memo(
|
||||
({ responseMessages, isCompletedStream }) => {
|
||||
// const [testMessages, setMessages] = useState<BusterChatMessageResponse[]>(responseMessages);
|
||||
// const replicaOfMessages = useRef<string>('');
|
||||
|
||||
// useEffect(() => {
|
||||
// setMessages(responseMessages);
|
||||
// replicaOfMessages.current =
|
||||
// (responseMessages as BusterChatMessage_text[])[0]?.message ||
|
||||
// (responseMessages as BusterChatMessage_text[])[0]?.message_chunk ||
|
||||
// '';
|
||||
// }, [responseMessages]);
|
||||
|
||||
// const firstMessageId = testMessages[0]?.id;
|
||||
// useHotkeys('x', () => {
|
||||
// const threeRandomWords = ' ' + faker.lorem.words(6) + ' swag';
|
||||
// setMessages((prevMessages) => {
|
||||
// return prevMessages.map((message) => {
|
||||
// if (message.id === firstMessageId) {
|
||||
// replicaOfMessages.current = replicaOfMessages.current + threeRandomWords;
|
||||
// return {
|
||||
// ...message,
|
||||
// message_chunk: threeRandomWords
|
||||
// };
|
||||
// }
|
||||
// return message;
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
// useHotkeys('z', () => {
|
||||
// setMessages((prevMessages) => {
|
||||
// return prevMessages.map((message) => {
|
||||
// if (message.id === firstMessageId) {
|
||||
// return { ...message, message: replicaOfMessages.current };
|
||||
// }
|
||||
|
||||
// return message;
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
|
||||
return (
|
||||
<MessageContainer>
|
||||
{responseMessages.map((responseMessage) => {
|
||||
const ChatResponseMessage = ChatResponseMessageRecord[responseMessage.type];
|
||||
return (
|
||||
<ChatResponseMessage
|
||||
key={responseMessage.id}
|
||||
responseMessage={responseMessage}
|
||||
isCompletedStream={isCompletedStream}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</MessageContainer>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
ChatResponseMessages.displayName = 'ChatResponseMessages';
|
||||
|
|
|
@ -0,0 +1,6 @@
|
|||
export const animationConfig = {
|
||||
initial: { opacity: 0 },
|
||||
animate: { opacity: 1 },
|
||||
exit: { opacity: 0 },
|
||||
transition: { duration: 0.675 }
|
||||
};
|
|
@ -1,6 +1,6 @@
|
|||
'use client';
|
||||
|
||||
import React, { useMemo, useRef, useState } from 'react';
|
||||
import React, { useRef } from 'react';
|
||||
import { AppSplitter, AppSplitterRef } from '@/components/layout/AppSplitter';
|
||||
import { ChatContainer } from './ChatContainer';
|
||||
import { FileContainer } from './FileContainer';
|
||||
|
|
|
@ -5,9 +5,12 @@ import {
|
|||
useContextSelector
|
||||
} from '@fluentui/react-context-selector';
|
||||
import { useBusterWebSocket } from '../BusterWebSocket';
|
||||
import type { BusterChatAsset, IBusterChat } from '@/api/buster_socket/chats';
|
||||
import type { BusterChatAsset, BusterChat } from '@/api/buster_socket/chats';
|
||||
import { useMemoizedFn, useUnmount } from 'ahooks';
|
||||
import type { FileType } from '@/api/buster_socket/chats';
|
||||
import { MOCK_CHAT } from './MOCK_CHAT';
|
||||
import { IBusterChat } from './interfaces';
|
||||
import { chatUpgrader } from './helpers';
|
||||
|
||||
export const useBusterChat = () => {
|
||||
const busterSocket = useBusterWebSocket();
|
||||
|
@ -20,12 +23,13 @@ export const useBusterChat = () => {
|
|||
|
||||
// LISTENERS
|
||||
|
||||
const _onGetChat = useMemoizedFn((chat: IBusterChat) => {
|
||||
chatsRef.current[chat.id] = chat;
|
||||
const _onGetChat = useMemoizedFn((chat: BusterChat): IBusterChat => {
|
||||
const upgradedChat = chatUpgrader(chat);
|
||||
chatsRef.current[chat.id] = upgradedChat;
|
||||
startTransition(() => {
|
||||
//just used to trigger UI update
|
||||
});
|
||||
return chat;
|
||||
return upgradedChat;
|
||||
});
|
||||
|
||||
const _onGetChatAsset = useMemoizedFn((asset: BusterChatAsset) => {
|
||||
|
@ -46,18 +50,17 @@ export const useBusterChat = () => {
|
|||
});
|
||||
|
||||
const subscribeToChat = useMemoizedFn(({ chatId }: { chatId: string }) => {
|
||||
return busterSocket.emitAndOnce({
|
||||
emitEvent: {
|
||||
route: '/chats/get',
|
||||
payload: {
|
||||
id: chatId
|
||||
}
|
||||
},
|
||||
responseEvent: {
|
||||
route: '/chats/get:getChat',
|
||||
callback: _onGetChat
|
||||
}
|
||||
});
|
||||
_onGetChat(MOCK_CHAT);
|
||||
// return busterSocket.emitAndOnce({
|
||||
// emitEvent: {
|
||||
// route: '/chats/get',
|
||||
// payload: { id: chatId }
|
||||
// },
|
||||
// responseEvent: {
|
||||
// route: '/chats/get:getChat',
|
||||
// callback: _onGetChat
|
||||
// }
|
||||
// });
|
||||
});
|
||||
|
||||
const getChatAsset = useMemoizedFn(
|
||||
|
|
|
@ -21,7 +21,7 @@ const createMockUserMessage = (
|
|||
const createMockResponseMessageText = (): BusterChatMessage_text => ({
|
||||
id: faker.string.uuid(),
|
||||
type: 'text',
|
||||
message: faker.lorem.sentence(),
|
||||
message: '',
|
||||
message_chunk: faker.lorem.sentence()
|
||||
});
|
||||
|
||||
|
|
|
@ -0,0 +1,29 @@
|
|||
import type { BusterChat, BusterChatMessage } from '@/api/buster_socket/chats';
|
||||
import type { IBusterChat, IBusterChatMessage } from '../interfaces';
|
||||
|
||||
export const chatUpgrader = (
|
||||
chat: BusterChat,
|
||||
options?: { isNewChat?: boolean; isFollowupMessage?: boolean }
|
||||
): IBusterChat => {
|
||||
const { isNewChat = false, isFollowupMessage = false } = options || {};
|
||||
return {
|
||||
...chat,
|
||||
isNewChat,
|
||||
isFollowupMessage,
|
||||
messages: chat.messages.map((message) =>
|
||||
chatMessageUpgrader(message, { isCompletedStream: !isFollowupMessage && !isNewChat })
|
||||
)
|
||||
};
|
||||
};
|
||||
|
||||
export const chatMessageUpgrader = (
|
||||
message: BusterChatMessage,
|
||||
options?: { isCompletedStream?: boolean }
|
||||
): IBusterChatMessage => {
|
||||
const { isCompletedStream = true } = options || {};
|
||||
|
||||
return {
|
||||
...message,
|
||||
isCompletedStream
|
||||
};
|
||||
};
|
|
@ -0,0 +1 @@
|
|||
export * from './chatUpgrader';
|
|
@ -1 +1,11 @@
|
|||
import type { BusterChat, BusterChatMessage } from '@/api/buster_socket/chats';
|
||||
|
||||
export interface IBusterChat extends BusterChat {
|
||||
isNewChat: boolean;
|
||||
isFollowupMessage: boolean;
|
||||
messages: IBusterChatMessage[];
|
||||
}
|
||||
|
||||
export interface IBusterChatMessage extends BusterChatMessage {
|
||||
isCompletedStream: boolean;
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue