From a2e85b70532ca204a8487b71ec7f4dad2e4b2562 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=AD=20Santos?= <140329135+itzraiss@users.noreply.github.com> Date: Sat, 10 Feb 2024 13:07:57 -0300 Subject: [PATCH] =?UTF-8?q?=E2=AC=A4=20style:=20Circular=20Streaming=20Cur?= =?UTF-8?q?sor=20(#1736)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Updated Style Cursor like ChatGPT * style(Markdown.tsx): add space before cursor when there is text * fix: revert OpenAIClient.tokens.js change * fix:(Markdown.tsx): revert change of unused file * fix(convos.spec.ts): test fix * chore: remove raw HTML for cursor animations --------- Co-authored-by: Danny Avila Co-authored-by: Danny Avila --- api/server/utils/handleText.js | 3 +- .../Chat/Messages/Content/Markdown.tsx | 68 +++++++------------ client/src/hooks/useChatHelpers.ts | 4 +- client/src/hooks/useMessageHandler.ts | 5 +- client/src/hooks/useSSE.ts | 4 -- client/src/style.css | 47 ++++++++++++- client/src/utils/convos.spec.ts | 2 +- 7 files changed, 75 insertions(+), 58 deletions(-) diff --git a/api/server/utils/handleText.js b/api/server/utils/handleText.js index b8d1710662..3cb5bfa148 100644 --- a/api/server/utils/handleText.js +++ b/api/server/utils/handleText.js @@ -1,7 +1,6 @@ const partialRight = require('lodash/partialRight'); const { sendMessage } = require('./streamResponse'); const { getCitations, citeText } = require('./citations'); -const cursor = ''; const citationRegex = /\[\^\d+?\^]/g; const addSpaceIfNeeded = (text) => (text.length > 0 && !text.endsWith(' ') ? text + ' ' : text); @@ -51,7 +50,7 @@ const createOnProgress = ({ generation = '', onProgress: _onProgress }) => { const sendIntermediateMessage = (res, payload, extraTokens = '') => { tokens += extraTokens; sendMessage(res, { - text: tokens?.length === 0 ? cursor : tokens, + text: tokens?.length === 0 ? '' : tokens, message: true, initial: i === 0, ...payload, diff --git a/client/src/components/Chat/Messages/Content/Markdown.tsx b/client/src/components/Chat/Messages/Content/Markdown.tsx index b32d62b4d8..e1d0268b5d 100644 --- a/client/src/components/Chat/Messages/Content/Markdown.tsx +++ b/client/src/components/Chat/Messages/Content/Markdown.tsx @@ -1,14 +1,14 @@ -import { useRecoilValue } from 'recoil'; -import React, { useState, useEffect } from 'react'; -import type { TMessage } from 'librechat-data-provider'; -import rehypeHighlight from 'rehype-highlight'; -import type { PluggableList } from 'unified'; -import ReactMarkdown from 'react-markdown'; -import supersub from 'remark-supersub'; -import rehypeKatex from 'rehype-katex'; -import remarkMath from 'remark-math'; +import { memo } from 'react'; import remarkGfm from 'remark-gfm'; import rehypeRaw from 'rehype-raw'; +import remarkMath from 'remark-math'; +import supersub from 'remark-supersub'; +import rehypeKatex from 'rehype-katex'; +import { useRecoilValue } from 'recoil'; +import ReactMarkdown from 'react-markdown'; +import rehypeHighlight from 'rehype-highlight'; +import type { TMessage } from 'librechat-data-provider'; +import type { PluggableList } from 'unified'; import CodeBlock from '~/components/Messages/Content/CodeBlock'; import { langSubset, validateIframe, processLaTeX } from '~/utils'; import { useChatContext } from '~/Providers'; @@ -26,7 +26,7 @@ type TContentProps = { showCursor?: boolean; }; -const code = React.memo(({ inline, className, children }: TCodeProps) => { +const code = memo(({ inline, className, children }: TCodeProps) => { const match = /language-(\w+)/.exec(className || ''); const lang = match && match[1]; @@ -37,48 +37,25 @@ const code = React.memo(({ inline, className, children }: TCodeProps) => { } }); -const p = React.memo(({ children }: { children: React.ReactNode }) => { +const p = memo(({ children }: { children: React.ReactNode }) => { return

{children}

; }); -const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => { - const [cursor, setCursor] = useState('█'); +const cursor = ' ⬤'; +const Markdown = memo(({ content, message, showCursor }: TContentProps) => { const { isSubmitting, latestMessage } = useChatContext(); const LaTeXParsing = useRecoilValue(store.LaTeXParsing); - const isInitializing = content === ''; + const isInitializing = content === ''; const { isEdited, messageId } = message ?? {}; const isLatestMessage = messageId === latestMessage?.messageId; - const _content = content?.replace('z-index: 1;', '') ?? ''; - const currentContent = LaTeXParsing ? processLaTeX(_content) : _content; - - useEffect(() => { - let timer1: NodeJS.Timeout, timer2: NodeJS.Timeout; - - if (!showCursor) { - setCursor('ㅤ'); - return; - } - - if (isSubmitting && isLatestMessage) { - timer1 = setInterval(() => { - setCursor('ㅤ'); - timer2 = setTimeout(() => { - setCursor('█'); - }, 200); - }, 1000); - } else { - setCursor('ㅤ'); - } - - // This is the cleanup function that React will run when the component unmounts - return () => { - clearInterval(timer1); - clearTimeout(timer2); - }; - }, [isSubmitting, isLatestMessage, showCursor]); + let currentContent = content; + if (!isInitializing) { + currentContent = currentContent?.replace('z-index: 1;', '') ?? ''; + currentContent = LaTeXParsing ? processLaTeX(currentContent) : currentContent; + } const rehypePlugins: PluggableList = [ [rehypeKatex, { output: 'mathml' }], @@ -93,6 +70,11 @@ const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => [rehypeRaw], ]; + if (isInitializing) { + rehypePlugins.pop(); + return ; + } + let isValidIframe: string | boolean | null = false; if (!isEdited) { isValidIframe = validateIframe(currentContent); @@ -116,7 +98,7 @@ const Markdown = React.memo(({ content, message, showCursor }: TContentProps) => } } > - {isLatestMessage && isSubmitting && !isInitializing + {isLatestMessage && isSubmitting && !isInitializing && showCursor ? currentContent + cursor : currentContent} diff --git a/client/src/hooks/useChatHelpers.ts b/client/src/hooks/useChatHelpers.ts index fd48524631..c807669268 100644 --- a/client/src/hooks/useChatHelpers.ts +++ b/client/src/hooks/useChatHelpers.ts @@ -179,9 +179,7 @@ export default function useChatHelpers(index = 0, paramId: string | undefined) { // construct the placeholder response message const generation = editedText ?? latestMessage?.text ?? ''; - const responseText = isEditOrContinue - ? generation - : ''; + const responseText = isEditOrContinue ? generation : ''; const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null; const initialResponse: TMessage = { diff --git a/client/src/hooks/useMessageHandler.ts b/client/src/hooks/useMessageHandler.ts index b39962a252..63757f9fbb 100644 --- a/client/src/hooks/useMessageHandler.ts +++ b/client/src/hooks/useMessageHandler.ts @@ -87,9 +87,7 @@ const useMessageHandler = () => { // construct the placeholder response message const generation = editedText ?? latestMessage?.text ?? ''; - const responseText = isEditOrContinue - ? generation - : ''; + const responseText = isEditOrContinue ? generation : ''; const responseMessageId = editedMessageId ?? latestMessage?.messageId ?? null; const initialResponse: TMessage = { @@ -99,7 +97,6 @@ const useMessageHandler = () => { messageId: responseMessageId ?? `${isRegenerate ? messageId : fakeMessageId}_`, conversationId, unfinished: false, - submitting: true, isCreatedByUser: false, isEdited: isEditOrContinue, error: false, diff --git a/client/src/hooks/useSSE.ts b/client/src/hooks/useSSE.ts index 13e04935b1..a59f330454 100644 --- a/client/src/hooks/useSSE.ts +++ b/client/src/hooks/useSSE.ts @@ -332,10 +332,6 @@ export default function useSSE(submission: TSubmission | null, index = 0) { } else if (response.status === 204) { const responseMessage = { ...submission.initialResponse, - text: submission.initialResponse.text.replace( - '', - '', - ), }; return { diff --git a/client/src/style.css b/client/src/style.css index 4a30683c8f..30e5589507 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -1314,7 +1314,7 @@ html { .result-streaming { -webkit-animation: blink 1s steps(5, start) infinite; animation: blink 1s steps(5, start) infinite; - content:"▋"; + content:"⬤ "; margin-left: 0.25rem; vertical-align: baseline; } @@ -1777,4 +1777,49 @@ html { .border-token-surface-tertiary { border-color: #ececf1; border-color: var(--surface-tertiary) +} + +@-webkit-keyframes pulseSize { + 0%, + to { + -webkit-transform:scaleX(1); + transform:scaleX(1) + } + 50% { + -webkit-transform:scale3d(1.25,1.25,1); + transform:scale3d(1.25,1.25,1) + } +} +@keyframes pulseSize { + 0%, + to { + -webkit-transform:scaleX(1); + transform:scaleX(1) + } + 50% { + -webkit-transform:scale3d(1.25,1.25,1); + transform:scale3d(1.25,1.25,1) + } +} +.result-thinking:empty:last-child:after { + -webkit-font-smoothing:subpixel-antialiased; + -webkit-animation:pulseSize 1.25s ease-in-out infinite; + animation:pulseSize 1.25s ease-in-out infinite; + -webkit-backface-visibility:hidden; + backface-visibility:hidden; + background-color:#0d0d0d; + background-color:var(--text-primary); + border-radius:50%; + box-sizing:border-box; + content:" "; + display:block; + height:12px; + position:absolute; + top:32px; + -webkit-transform:translateZ(0); + transform:translateZ(0); + -webkit-transform-origin:center; + transform-origin:center; + width:12px; + will-change:transform } \ No newline at end of file diff --git a/client/src/utils/convos.spec.ts b/client/src/utils/convos.spec.ts index 1d21d8654c..1ec1c00b1e 100644 --- a/client/src/utils/convos.spec.ts +++ b/client/src/utils/convos.spec.ts @@ -152,7 +152,7 @@ describe('Conversation Utilities with Fake Data', () => { const allConversations = pages.flatMap((p) => p.conversations); const grouped = groupConversationsByDate(allConversations); - expect(grouped).toHaveLength(1); + expect(grouped).toHaveLength(2); expect(grouped[0][1]).toBeInstanceOf(Array); }); });