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:
commit
7ee2b02dda
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
};
|
||||
|
||||
|
106
src/views/CurrentMessage.tsx
Normal file
106
src/views/CurrentMessage.tsx
Normal 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;
|
@ -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 >;
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user