diff --git a/src/views/ChatPanel.tsx b/src/views/ChatPanel.tsx index b7b6f48..37fb295 100644 --- a/src/views/ChatPanel.tsx +++ b/src/views/ChatPanel.tsx @@ -7,24 +7,16 @@ import { useListState, useResizeObserver, useTimeout, useViewportSize } from '@m import { IconPlayerStop, IconRotateDot } from '@tabler/icons-react'; import messageUtil from '@/util/MessageUtil'; import { useAppDispatch, useAppSelector } from '@/views/hooks'; +import CurrentMessage from "@/views/CurrentMessage"; -import { - setValue -} from './inputSlice'; import { reGenerating, stopGenerating, startResponsing, happendError, - newMessage, - updateMessage, - shiftMessage, selectGenerating, - selectCurrentMessage, selectErrorMessage, - selectMessages, selectIsBottom, - selectPageIndex, selectIsLastPage, onMessagesBottom, onMessagesTop, @@ -77,7 +69,7 @@ const StopButton = () => { } }} onClick={() => { - dispatch(stopGenerating()); + dispatch(stopGenerating({ hasDone: false })); messageUtil.sendMessage({ command: 'stopDevChat' }); @@ -90,12 +82,9 @@ const StopButton = () => { const chatPanel = () => { const dispatch = useAppDispatch(); const generating = useAppSelector(selectGenerating); - const currentMessage = useAppSelector(selectCurrentMessage); - const errorMessage = useAppSelector(selectErrorMessage); - const messages = useAppSelector(selectMessages); const isBottom = useAppSelector(selectIsBottom); const isLastPage = useAppSelector(selectIsLastPage); - const pageIndex = useAppSelector(selectPageIndex); + const errorMessage = useAppSelector(selectErrorMessage); const [chatContainerRef, chatContainerRect] = useResizeObserver(); const scrollViewport = useRef(null); const { height, width } = useViewportSize(); @@ -133,9 +122,10 @@ const chatPanel = () => { dispatch(fetchHistoryMessages({ pageIndex: 0 })); messageUtil.registerHandler('receiveMessagePartial', (message: { text: string; }) => { dispatch(startResponsing(message.text)); + timer.start(); }); messageUtil.registerHandler('receiveMessage', (message: { text: string; isError: boolean }) => { - dispatch(stopGenerating()); + dispatch(stopGenerating({ hasDone: true })); if (message.isError) { dispatch(happendError(message.text)); } @@ -146,28 +136,6 @@ const chatPanel = () => { }; }, []); - useEffect(() => { - if (generating) { - // new a bot message - dispatch(newMessage({ type: 'bot', message: currentMessage })); - } - }, [generating]); - - // Add the received message to the chat UI as a bot message - useEffect(() => { - const lastIndex = messages?.length - 1; - const lastMessage = messages[lastIndex]; - if (currentMessage && lastMessage?.type === 'bot') { - // update the last one bot message - // messageHandlers.setItem(); - dispatch(updateMessage({ - index: lastIndex, - newMessage: { type: 'bot', message: currentMessage } - })); - } - timer.start(); - }, [currentMessage]); - return ( { viewportRef={scrollViewport}> + {errorMessage && {errorMessage} - - } + } { > {messageText} - :
{messageText}
+ : +
{messageText}
+
); }; diff --git a/src/views/CurrentMessage.tsx b/src/views/CurrentMessage.tsx new file mode 100644 index 0000000..a155040 --- /dev/null +++ b/src/views/CurrentMessage.tsx @@ -0,0 +1,106 @@ + +import React, { useEffect } from "react"; +import { keyframes } from "@emotion/react"; +import { Container, Text } from "@mantine/core"; +import { + selectResponsed, +} from './chatSlice'; +import { useAppDispatch, useAppSelector } from '@/views/hooks'; +import CodeBlock from "@/views/CodeBlock"; + +import { + newMessage, + updateLastMessage, + selectGenerating, + selectCurrentMessage, + selecLastMessage, + selecHasDone, +} from './chatSlice'; + +const MessageBlink = () => { + const responsed = useAppSelector(selectResponsed); + + const blink = keyframes({ + '50%': { opacity: 0 }, + }); + + return |; +}; + +const getBlocks = (message) => { + const messageText = message || ''; + const regex = /```([\s\S]+?)```/g; + + let match; + let lastIndex = 0; + const blocks: string[] = []; + + while ((match = regex.exec(messageText))) { + const unmatchedText = messageText.substring(lastIndex, match.index); + const matchedText = match[0]; + blocks.push(unmatchedText, matchedText); + lastIndex = regex.lastIndex; + } + + const unmatchedText = messageText.substring(lastIndex); + blocks.push(unmatchedText); + + return blocks; +} + +const CurrentMessage = (props: any) => { + const { width } = props; + + const dispatch = useAppDispatch(); + const currentMessage = useAppSelector(selectCurrentMessage); + const lastMessage = useAppSelector(selecLastMessage); + const generating = useAppSelector(selectGenerating); + const hasDone = useAppSelector(selecHasDone); + + // split blocks + const messageBlocks = getBlocks(currentMessage); + const lastMessageBlocks = getBlocks(lastMessage?.message); + const fixedCount = lastMessageBlocks.length; + const receivedCount = messageBlocks.length; + const renderBlocks = messageBlocks.splice(-1); + + useEffect(() => { + if (generating) { + // new a bot message + dispatch(newMessage({ type: 'bot', message: currentMessage })); + } + if (hasDone) { + // update the last one bot message + dispatch(updateLastMessage({ type: 'bot', message: currentMessage })); + } + }, [generating, hasDone]); + + useEffect(() => { + if (receivedCount - fixedCount >= 1) { + dispatch(updateLastMessage({ type: 'bot', message: currentMessage })); + } + }, [currentMessage]); + + return generating + ? + + + + : <>; +}; + +export default CurrentMessage; \ No newline at end of file diff --git a/src/views/MessageContainer.tsx b/src/views/MessageContainer.tsx index 83c3ec9..fba2f6f 100644 --- a/src/views/MessageContainer.tsx +++ b/src/views/MessageContainer.tsx @@ -1,5 +1,5 @@ -import { keyframes } from "@emotion/react"; -import { Center, Text, Flex, Avatar, Accordion, Box, Stack, Container, Divider, ActionIcon, Tooltip } from "@mantine/core"; + +import { Center, Text, Flex, Avatar, Accordion, Box, Stack, Container, Divider, ActionIcon, Tooltip, CopyButton } from "@mantine/core"; import React from "react"; import CodeBlock from "@/views/CodeBlock"; @@ -7,12 +7,10 @@ import CodeBlock from "@/views/CodeBlock"; import SvgAvatarDevChat from '@/views/avatar_devchat.svg'; // @ts-ignore import SvgAvatarUser from '@/views/avatar_spaceman.png'; -import { IconCheck, IconCopy } from "@tabler/icons-react"; +import { IconCheck, IconCopy, Icon360 } from "@tabler/icons-react"; import { useAppDispatch, useAppSelector } from '@/views/hooks'; import { - selectGenerating, - selectResponsed, selectMessages, } from './chatSlice'; import { @@ -21,28 +19,6 @@ import { } from './inputSlice'; -const MessageBlink = (props: any) => { - const { messageType, lastMessage } = props; - - const generating = useAppSelector(selectGenerating); - const responsed = useAppSelector(selectResponsed); - - const blink = keyframes({ - '50%': { opacity: 0 }, - }); - - return (generating && messageType === 'bot' && lastMessage - ? | - : <>); -}; - const MessageContext = (props: any) => { const { contexts } = props; return (contexts && @@ -122,7 +98,7 @@ const MessageContext = (props: any) => { const MessageHeader = (props: any) => { const { type, message, contexts } = props; const dispatch = useAppDispatch(); - const [refilled, setRefilled] = React.useState(false); + const [done, setDone] = React.useState(false); return ( { } {type === 'bot' ? 'DevChat' : 'User'} {type === 'user' - ? + ? { dispatch(setValue(message)); dispatch(setContexts(contexts)); - setRefilled(true); - setTimeout(() => { setRefilled(false); }, 2000); + setDone(true); + setTimeout(() => { setDone(false); }, 2000); }}> - {refilled ? : } + {done ? : } - : <> + : + {({ copied, copy }) => ( + + + {copied ? : } + + + )} + } - ); + ); }; const MessageContainer = (props: any) => { @@ -194,7 +178,6 @@ const MessageContainer = (props: any) => { }}> -
{index !== messages.length - 1 && } ; diff --git a/src/views/chatSlice.ts b/src/views/chatSlice.ts index a64f350..913e4b3 100644 --- a/src/views/chatSlice.ts +++ b/src/views/chatSlice.ts @@ -24,7 +24,9 @@ export const chatSlice = createSlice({ initialState: { generating: false, responsed: false, + lastMessage: null, currentMessage: '', + hasDone: false, errorMessage: '', messages: [], pageIndex: 0, @@ -36,6 +38,7 @@ export const chatSlice = createSlice({ startGenerating: (state, action) => { state.generating = true; state.responsed = false; + state.hasDone = false; state.errorMessage = ''; state.currentMessage = ''; messageUtil.sendMessage({ @@ -46,6 +49,7 @@ export const chatSlice = createSlice({ reGenerating: (state) => { state.generating = true; state.responsed = false; + state.hasDone = false; state.errorMessage = ''; state.currentMessage = ''; state.messages.pop(); @@ -53,9 +57,10 @@ export const chatSlice = createSlice({ command: 'regeneration' }); }, - stopGenerating: (state) => { + stopGenerating: (state, action) => { state.generating = false; state.responsed = false; + state.hasDone = action.payload.hasDone; }, startResponsing: (state, action) => { state.responsed = true; @@ -63,9 +68,11 @@ export const chatSlice = createSlice({ }, newMessage: (state, action) => { state.messages.push(action.payload); + state.lastMessage = action.payload; }, - updateMessage: (state, action) => { - state.messages[action.payload.index] = action.payload.newMessage; + updateLastMessage: (state, action) => { + state.messages[state.messages.length - 1] = action.payload; + state.lastMessage = action.payload; }, shiftMessage: (state) => { state.messages.splice(0, 1); @@ -122,6 +129,8 @@ export const chatSlice = createSlice({ export const selectGenerating = (state: RootState) => state.chat.generating; export const selectResponsed = (state: RootState) => state.chat.responsed; +export const selecHasDone = (state: RootState) => state.chat.hasDone; +export const selecLastMessage = (state: RootState) => state.chat.lastMessage; export const selectCurrentMessage = (state: RootState) => state.chat.currentMessage; export const selectErrorMessage = (state: RootState) => state.chat.errorMessage; export const selectMessages = (state: RootState) => state.chat.messages; @@ -141,7 +150,7 @@ export const { shiftMessage, popMessage, clearMessages, - updateMessage, + updateLastMessage, onMessagesTop, onMessagesBottom, onMessagesMiddle,