Merge pull request #201 from devchat-ai/194-split-rendering-of-markdown-in-message-body

194 split rendering of markdown in message body
This commit is contained in:
Rankin Zheng 2023-07-06 07:40:01 +08:00 committed by GitHub
commit 7ee2b02dda
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 156 additions and 80 deletions

View File

@ -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<HTMLDivElement>(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 (
<Container
ref={chatContainerRef}
@ -191,11 +159,11 @@ const chatPanel = () => {
viewportRef={scrollViewport}>
<MessageContainer
width={chatContainerRect.width} />
<CurrentMessage width={chatContainerRect.width} />
{errorMessage &&
<Alert styles={{ message: { fontSize: 'var(--vscode-editor-font-size)' } }} w={chatContainerRect.width} mb={20} color="gray" variant="filled">
{errorMessage}
</Alert>
}
</Alert>}
</ScrollArea>
<Stack
spacing={5}

View File

@ -1,4 +1,4 @@
import { Tooltip, ActionIcon, CopyButton, Flex } from "@mantine/core";
import { Tooltip, ActionIcon, CopyButton, Flex, Container } from "@mantine/core";
import { IconCheck, IconGitCommit, IconFileDiff, IconColumnInsertRight, IconReplace, IconCopy } from "@tabler/icons-react";
import React, { useState } from "react";
import ReactMarkdown from "react-markdown";
@ -147,7 +147,17 @@ const CodeBlock = (props: any) => {
>
{messageText}
</ReactMarkdown >
: <pre>{messageText}</pre>
: <Container
sx={{
margin: 0,
padding: 0,
pre: {
whiteSpace: 'pre-wrap',
wordBreak: 'break-word',
},
}}>
<pre>{messageText}</pre>
</Container>
);
};

View File

@ -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 <Text sx={{
animation: `${blink} 0.5s infinite;`,
width: 5,
marginTop: responsed ? 0 : '1em',
backgroundColor: 'black',
display: 'block'
}}>|</Text>;
};
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
? <Container
sx={{
margin: 0,
padding: 0,
width: width,
pre: {
whiteSpace: 'break-spaces'
},
}}>
<CodeBlock messageText={renderBlocks.join('\n\n')} messageType="bot" />
<MessageBlink />
</Container>
: <></>;
};
export default CurrentMessage;

View File

@ -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
? <Text sx={{
animation: `${blink} 0.5s infinite;`,
width: 5,
marginTop: responsed ? 0 : '1em',
backgroundColor: 'black',
display: 'block'
}}>|</Text>
: <></>);
};
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 (<Flex
m='10px 0 10px 0'
gap="sm"
@ -145,20 +121,28 @@ const MessageHeader = (props: any) => {
}
<Text weight='bold'>{type === 'bot' ? 'DevChat' : 'User'}</Text>
{type === 'user'
? <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={refilled ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
? <Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={done ? 'Refilled' : 'Refill prompt'} withArrow position="left" color="gray">
<ActionIcon size='sm' style={{ marginLeft: 'auto' }}
onClick={() => {
dispatch(setValue(message));
dispatch(setContexts(contexts));
setRefilled(true);
setTimeout(() => { setRefilled(false); }, 2000);
setDone(true);
setTimeout(() => { setDone(false); }, 2000);
}}>
{refilled ? <IconCheck size="1rem" /> : <IconCopy size="1.125rem" />}
{done ? <IconCheck size="1rem" /> : <Icon360 size="1.125rem" />}
</ActionIcon>
</Tooltip>
: <></>
: <CopyButton value={message} timeout={2000}>
{({ copied, copy }) => (
<Tooltip sx={{ padding: '3px', fontSize: 'var(--vscode-editor-font-size)' }} label={copied ? 'Copied' : 'Copy message'} withArrow position="left" color="gray">
<ActionIcon size='xs' color={copied ? 'teal' : 'gray'} onClick={copy} style={{ marginLeft: 'auto' }}>
{copied ? <IconCheck size="1rem" /> : <IconCopy size="1rem" />}
</ActionIcon>
</Tooltip>
)}
</CopyButton>
}
</Flex>);
</Flex >);
};
const MessageContainer = (props: any) => {
@ -194,7 +178,6 @@ const MessageContainer = (props: any) => {
}}>
<MessageContext key={`message-context-${index}`} contexts={contexts} />
<CodeBlock key={`message-codeblock-${index}`} messageType={messageType} messageText={messageText} />
<MessageBlink key={`message-blink-${index}`} messageType={messageType} lastMessage={index === messages.length - 1} />
</Container >
{index !== messages.length - 1 && <Divider my={3} key={`message-divider-${index}`} />}
</Stack >;

View File

@ -24,7 +24,9 @@ export const chatSlice = createSlice({
initialState: {
generating: false,
responsed: false,
lastMessage: <any>null,
currentMessage: '',
hasDone: false,
errorMessage: '',
messages: <any>[],
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,