(null);
+ const [highlighted, setHighlighted] = useState(false);
useEffect(() => {
if (codeRef.current) {
@@ -27,23 +30,27 @@ const CodeSnippet = ({
codeRef.current.getAttribute('data-highlighted') === 'yes';
if (!highlighted) {
hljs.highlightElement(codeRef.current);
+ setHighlighted(true);
}
}
}, []);
+ const streaming = highlighted && children.includes(CURSOR_PLACEHOLDER);
+
return (
- {children}
+ {children.replace(CURSOR_PLACEHOLDER, '')}
+ {streaming ? : null}
);
@@ -66,7 +73,7 @@ const Code = ({ children, ...props }: any) => {
theme.spacing(1),
paddingRight: '2.5em',
minHeight: '20px',
@@ -97,8 +104,8 @@ const Code = ({ children, ...props }: any) => {
sx={{
justifyContent: 'space-between',
alignItems: 'center',
- borderTopLeftRadius: '4px',
- borderTopRightRadius: '4px',
+ borderTopLeftRadius: '0.7rem',
+ borderTopRightRadius: '0.7rem',
color: 'text.secondary',
background: isDarkMode ? grey[900] : grey[200]
}}
diff --git a/frontend/src/components/molecules/Markdown.tsx b/frontend/src/components/molecules/Markdown.tsx
index e1c461f2e9..77d6db4451 100644
--- a/frontend/src/components/molecules/Markdown.tsx
+++ b/frontend/src/components/molecules/Markdown.tsx
@@ -6,6 +6,7 @@ import rehypeKatex from 'rehype-katex';
import rehypeRaw from 'rehype-raw';
import remarkGfm from 'remark-gfm';
import remarkMath from 'remark-math';
+import { visit } from 'unist-util-visit';
import Link from '@mui/material/Link';
import Paper from '@mui/material/Paper';
@@ -22,6 +23,8 @@ import { ElementRef } from 'components/molecules/messages/components/ElementRef'
import type { IMessageElement } from 'client-types/';
+import BlinkingCursor from './BlinkingCursor';
+
interface Props {
allowHtml?: boolean;
latex?: boolean;
@@ -29,6 +32,52 @@ interface Props {
children: string;
}
+const cursorPlugin = () => {
+ return (tree: any) => {
+ visit(tree, 'text', (node: any, index, parent) => {
+ const placeholderPattern = /\u200B/g;
+ const matches = [...(node.value?.matchAll(placeholderPattern) || [])];
+
+ if (matches.length > 0) {
+ const newNodes: any[] = [];
+ let lastIndex = 0;
+
+ matches.forEach((match) => {
+ const [fullMatch] = match;
+ const startIndex = match.index!;
+ const endIndex = startIndex + fullMatch.length;
+
+ if (startIndex > lastIndex) {
+ newNodes.push({
+ type: 'text',
+ value: node.value!.slice(lastIndex, startIndex)
+ });
+ }
+
+ newNodes.push({
+ type: 'blinkingCursor',
+ data: {
+ hName: 'blinkingCursor',
+ hProperties: { text: 'Blinking Cursor' }
+ }
+ });
+
+ lastIndex = endIndex;
+ });
+
+ if (lastIndex < node.value!.length) {
+ newNodes.push({
+ type: 'text',
+ value: node.value!.slice(lastIndex)
+ });
+ }
+
+ parent!.children.splice(index, 1, ...newNodes);
+ }
+ });
+ };
+};
+
function Markdown({ refElements, allowHtml, latex, children }: Props) {
const rehypePlugins = useMemo(() => {
let rehypePlugins: PluggableList = [];
@@ -42,10 +91,10 @@ function Markdown({ refElements, allowHtml, latex, children }: Props) {
}, [allowHtml, latex]);
const remarkPlugins = useMemo(() => {
- let remarkPlugins: PluggableList = [remarkGfm as any];
+ let remarkPlugins: PluggableList = [cursorPlugin, remarkGfm as any];
if (latex) {
- remarkPlugins = [remarkMath as any, ...remarkPlugins];
+ remarkPlugins = [...remarkPlugins, remarkMath as any];
}
return remarkPlugins;
}, [latex]);
@@ -119,7 +168,9 @@ function Markdown({ refElements, allowHtml, latex, children }: Props) {
tbody({ children, ...props }) {
// @ts-ignore
return {children};
- }
+ },
+ // @ts-expect-error custom plugin
+ blinkingCursor: () =>
}}
>
{children}
diff --git a/frontend/src/components/molecules/messages/Message.tsx b/frontend/src/components/molecules/messages/Message.tsx
index b2bf79d86f..89e748cd6b 100644
--- a/frontend/src/components/molecules/messages/Message.tsx
+++ b/frontend/src/components/molecules/messages/Message.tsx
@@ -16,6 +16,7 @@ import { useLayoutMaxWidth } from 'hooks/useLayoutMaxWidth';
import type { IAction, IMessageElement, IStep } from 'client-types/';
+import BlinkingCursor from '../BlinkingCursor';
import { Messages } from './Messages';
interface Props {
@@ -62,11 +63,13 @@ const Message = memo(
}
const isAsk = message.waitForAnswer;
+ const isUserMessage = message.type === 'user_message';
return (
@@ -96,31 +99,81 @@ const Message = memo(
overflowX: 'auto'
}}
>
-
-
-
+ {isUserMessage ? (
+
+
+
+
setShowDetails(!showDetails)}
loading={isRunning && isLast}
/>
- {!isRunning && isLast && isAsk && (
-
- )}
- {actions?.length ? (
-
- ) : null}
-
-
-
+
+ ) : (
+
+
+
+ setShowDetails(!showDetails)}
+ loading={isRunning && isLast}
+ />
+ {!isRunning && isLast && isAsk && (
+
+ )}
+ {actions?.length ? (
+
+ ) : null}
+
+
+
+ )}
+ {isLast && isRunning && !message.streaming && (
+
+
+
+ )}
{message.steps && showDetails && (
();
useEffect(() => {
- if (!ref.current || !autoScroll) {
- return;
- }
- ref.current.scrollTop = ref.current.scrollHeight;
+ setTimeout(() => {
+ if (!ref.current || !autoScroll) {
+ return;
+ }
+ ref.current.scrollTop = ref.current.scrollHeight;
+ }, 0);
}, [messages, autoScroll]);
const handleScroll = () => {
diff --git a/frontend/src/components/molecules/messages/components/DetailsButton.tsx b/frontend/src/components/molecules/messages/components/DetailsButton.tsx
index 5f58d30874..8ae15c5be8 100644
--- a/frontend/src/components/molecules/messages/components/DetailsButton.tsx
+++ b/frontend/src/components/molecules/messages/components/DetailsButton.tsx
@@ -1,13 +1,12 @@
import { MessageContext } from 'contexts/MessageContext';
import { useContext } from 'react';
-import ExpandLess from '@mui/icons-material/ExpandLess';
-import ExpandMore from '@mui/icons-material/ExpandMore';
-import CircularProgress from '@mui/material/CircularProgress';
-
import { GreyButton } from 'components/atoms/buttons/GreyButton';
import { Translator } from 'components/i18n';
+import ChevronDownIcon from 'assets/chevronDown';
+import ChevronUpIcon from 'assets/chevronUp';
+
import type { IStep } from 'client-types/';
interface Props {
@@ -19,7 +18,6 @@ interface Props {
const DetailsButton = ({ message, opened, onClick, loading }: Props) => {
const messageContext = useContext(MessageContext);
-
const nestedCount = message.steps?.length;
const nested = !!nestedCount && !messageContext.hideCot;
@@ -27,14 +25,7 @@ const DetailsButton = ({ message, opened, onClick, loading }: Props) => {
const tool = lastStep ? lastStep.name : undefined;
- const content = message.output;
-
- const showDefaultLoader =
- loading && (!content || (messageContext.hideCot && !message.streaming));
-
- const show = tool || showDefaultLoader;
-
- if (!show) {
+ if (!tool) {
return null;
}
@@ -46,14 +37,9 @@ const DetailsButton = ({ message, opened, onClick, loading }: Props) => {
const text = (
{loading ? (
- tool ? (
- <>
- {' '}
- {tool}
- >
- ) : (
-
- )
+ <>
+ {tool}
+ >
) : (
<>
{
: undefined
- }
variant="contained"
endIcon={
- nested && tool ? opened ? : : undefined
+ nested && tool ? (
+ opened ? (
+
+ ) : (
+
+ )
+ ) : undefined
}
onClick={tool ? onClick : undefined}
>
diff --git a/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx b/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx
index b13a23ec63..d9ccc16f7f 100644
--- a/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx
+++ b/frontend/src/components/molecules/messages/components/FeedbackButtons.tsx
@@ -3,11 +3,6 @@ import { useContext, useState } from 'react';
import { useMemo } from 'react';
import { useRecoilValue } from 'recoil';
-import StickyNote2Outlined from '@mui/icons-material/StickyNote2Outlined';
-import ThumbDownAlt from '@mui/icons-material/ThumbDownAlt';
-import ThumbDownAltOutlined from '@mui/icons-material/ThumbDownAltOutlined';
-import ThumbUpAlt from '@mui/icons-material/ThumbUpAlt';
-import ThumbUpAltOutlined from '@mui/icons-material/ThumbUpAltOutlined';
import IconButton from '@mui/material/IconButton';
import Stack from '@mui/material/Stack';
import Tooltip from '@mui/material/Tooltip';
@@ -18,6 +13,14 @@ import Dialog from 'components/atoms/Dialog';
import { AccentButton } from 'components/atoms/buttons/AccentButton';
import { TextInput } from 'components/atoms/inputs';
+import MessageBubbleIcon from 'assets/messageBubble';
+import {
+ ThumbDownFilledIcon,
+ ThumbDownIcon,
+ ThumbUpFilledIcon,
+ ThumbUpIcon
+} from 'assets/thumbs';
+
import type { IStep } from 'client-types/';
const ICON_SIZE = '16px';
@@ -36,8 +39,8 @@ const FeedbackButtons = ({ message }: Props) => {
const [feedback, setFeedback] = useState(message.feedback?.value);
const [comment, setComment] = useState(message.feedback?.comment);
- const DownIcon = feedback === 0 ? ThumbDownAlt : ThumbDownAltOutlined;
- const UpIcon = feedback === 1 ? ThumbUpAlt : ThumbUpAltOutlined;
+ const DownIcon = feedback === 0 ? ThumbDownFilledIcon : ThumbDownIcon;
+ const UpIcon = feedback === 1 ? ThumbUpFilledIcon : ThumbUpIcon;
const handleFeedbackChanged = (feedback?: number, comment?: string) => {
if (feedback === undefined) {
@@ -136,7 +139,7 @@ const FeedbackButtons = ({ message }: Props) => {
}}
className="feedback-comment-edit"
>
-
+