diff --git a/web/src/app/chat/ChatPage.tsx b/web/src/app/chat/ChatPage.tsx index e9676d28246..ac10e9875b5 100644 --- a/web/src/app/chat/ChatPage.tsx +++ b/web/src/app/chat/ChatPage.tsx @@ -755,7 +755,15 @@ export function ChatPage({ setAboveHorizon(scrollDist.current > 500); }; - scrollableDivRef?.current?.addEventListener("scroll", updateScrollTracking); + useEffect(() => { + const scrollableDiv = scrollableDivRef.current; + if (scrollableDiv) { + scrollableDiv.addEventListener("scroll", updateScrollTracking); + return () => { + scrollableDiv.removeEventListener("scroll", updateScrollTracking); + }; + } + }, []); const handleInputResize = () => { setTimeout(() => { @@ -1133,7 +1141,9 @@ export function ChatPage({ await delay(50); while (!stack.isComplete || !stack.isEmpty()) { - await delay(0.5); + if (stack.isEmpty()) { + await delay(0.5); + } if (!stack.isEmpty() && !controller.signal.aborted) { const packet = stack.nextPacket(); diff --git a/web/src/app/chat/message/CodeBlock.tsx b/web/src/app/chat/message/CodeBlock.tsx index 55a6ea7be32..66cc82a6e73 100644 --- a/web/src/app/chat/message/CodeBlock.tsx +++ b/web/src/app/chat/message/CodeBlock.tsx @@ -1,20 +1,22 @@ import React, { useState, ReactNode, useCallback, useMemo, memo } from "react"; import { FiCheck, FiCopy } from "react-icons/fi"; -const CODE_BLOCK_PADDING_TYPE = { padding: "1rem" }; +const CODE_BLOCK_PADDING = { padding: "1rem" }; interface CodeBlockProps { - className?: string | undefined; + className?: string; children?: ReactNode; - content: string; - [key: string]: any; + codeText: string; } +const MemoizedCodeLine = memo(({ content }: { content: ReactNode }) => ( + <>{content} +)); + export const CodeBlock = memo(function CodeBlock({ className = "", children, - content, - ...props + codeText, }: CodeBlockProps) { const [copied, setCopied] = useState(false); @@ -26,132 +28,99 @@ export const CodeBlock = memo(function CodeBlock({ .join(" "); }, [className]); - const codeText = useMemo(() => { - let codeText: string | null = null; - if ( - props.node?.position?.start?.offset && - props.node?.position?.end?.offset - ) { - codeText = content.slice( - props.node.position.start.offset, - props.node.position.end.offset - ); - codeText = codeText.trim(); - - // Find the last occurrence of closing backticks - const lastBackticksIndex = codeText.lastIndexOf("```"); - if (lastBackticksIndex !== -1) { - codeText = codeText.slice(0, lastBackticksIndex + 3); - } - - // Remove the language declaration and trailing backticks - const codeLines = codeText.split("\n"); - if ( - codeLines.length > 1 && - (codeLines[0].startsWith("```") || - codeLines[0].trim().startsWith("```")) - ) { - codeLines.shift(); // Remove the first line with the language declaration - if ( - codeLines[codeLines.length - 1] === "```" || - codeLines[codeLines.length - 1]?.trim() === "```" - ) { - codeLines.pop(); // Remove the last line with the trailing backticks - } - - const minIndent = codeLines - .filter((line) => line.trim().length > 0) - .reduce((min, line) => { - const match = line.match(/^\s*/); - return Math.min(min, match ? match[0].length : 0); - }, Infinity); - - const formattedCodeLines = codeLines.map((line) => - line.slice(minIndent) + const handleCopy = useCallback(() => { + if (!codeText) return; + navigator.clipboard.writeText(codeText).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [codeText]); + + const CopyButton = memo(() => ( +
+ {copied ? ( +
+ + Copied! +
+ ) : ( +
+ + Copy code +
+ )} +
+ )); + CopyButton.displayName = "CopyButton"; + + const CodeContent = memo(() => { + if (!language) { + if (typeof children === "string") { + return ( + + {children} + ); - codeText = formattedCodeLines.join("\n"); } - } - - // handle unknown languages. They won't have a `node.position.start.offset` - if (!codeText) { - const findTextNode = (node: any): string | null => { - if (node.type === "text") { - return node.value; - } - let finalResult = ""; - if (node.children) { - for (const child of node.children) { - const result = findTextNode(child); - if (result) { - finalResult += result; - } - } - } - return finalResult; - }; - - codeText = findTextNode(props.node); - } - - return codeText; - }, [content, props.node]); - - const handleCopy = useCallback( - (event: React.MouseEvent) => { - event.preventDefault(); - if (!codeText) { - return; - } - - navigator.clipboard.writeText(codeText).then(() => { - setCopied(true); - setTimeout(() => setCopied(false), 2000); - }); - }, - [codeText] - ); - - if (!language) { - if (typeof children === "string") { - return {children}; + return ( +
+          
+            {Array.isArray(children)
+              ? children.map((child, index) => (
+                  
+                ))
+              : children}
+          
+        
+ ); } return ( -
-        
-          {children}
+      
+        
+          {Array.isArray(children)
+            ? children.map((child, index) => (
+                
+              ))
+            : children}
         
       
); - } + }); + CodeContent.displayName = "CodeContent"; return (
-
- {language} - {codeText && ( -
- {copied ? ( -
- - Copied! -
- ) : ( -
- - Copy code -
- )} -
- )} -
-
-        {children}
-      
+ {language && ( +
+ {language} + {codeText && } +
+ )} +
); }); + +CodeBlock.displayName = "CodeBlock"; +MemoizedCodeLine.displayName = "MemoizedCodeLine"; diff --git a/web/src/app/chat/message/MemoizedTextComponents.tsx b/web/src/app/chat/message/MemoizedTextComponents.tsx index 4ab8bc810b2..9ab0e28e3ca 100644 --- a/web/src/app/chat/message/MemoizedTextComponents.tsx +++ b/web/src/app/chat/message/MemoizedTextComponents.tsx @@ -25,9 +25,9 @@ export const MemoizedLink = memo((props: any) => { } }); -export const MemoizedParagraph = memo(({ node, ...props }: any) => ( -

-)); +export const MemoizedParagraph = memo(({ ...props }: any) => { + return

; +}); MemoizedLink.displayName = "MemoizedLink"; MemoizedParagraph.displayName = "MemoizedParagraph"; diff --git a/web/src/app/chat/message/Messages.tsx b/web/src/app/chat/message/Messages.tsx index edb18138c79..e10f5cea03c 100644 --- a/web/src/app/chat/message/Messages.tsx +++ b/web/src/app/chat/message/Messages.tsx @@ -54,6 +54,7 @@ import RegenerateOption from "../RegenerateOption"; import { LlmOverride } from "@/lib/hooks"; import { ContinueGenerating } from "./ContinueMessage"; import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents"; +import { extractCodeText } from "./codeUtils"; const TOOLS_WITH_CUSTOM_HANDLING = [ SEARCH_TOOL_NAME, @@ -253,6 +254,40 @@ export const AIMessage = ({ new Set((docs || []).map((doc) => doc.source_type)) ).slice(0, 3); + const markdownComponents = useMemo( + () => ({ + a: MemoizedLink, + p: MemoizedParagraph, + code: ({ node, inline, className, children, ...props }: any) => { + const codeText = extractCodeText( + node, + finalContent as string, + children + ); + + return ( + + {children} + + ); + }, + }), + [messageId, content] + ); + + const renderedMarkdown = useMemo(() => { + return ( + + {finalContent as string} + + ); + }, [finalContent]); + const includeMessageSwitcher = currentMessageInd !== undefined && onMessageSelection && @@ -352,27 +387,7 @@ export const AIMessage = ({ {typeof content === "string" ? (

- ( - - ), - }} - remarkPlugins={[remarkGfm]} - rehypePlugins={[ - [rehypePrism, { ignoreMissing: true }], - ]} - > - {finalContent as string} - + {renderedMarkdown}
) : ( content diff --git a/web/src/app/chat/message/codeUtils.ts b/web/src/app/chat/message/codeUtils.ts new file mode 100644 index 00000000000..2aaae71bc82 --- /dev/null +++ b/web/src/app/chat/message/codeUtils.ts @@ -0,0 +1,47 @@ +export function extractCodeText( + node: any, + content: string, + children: React.ReactNode +): string { + let codeText: string | null = null; + if ( + node?.position?.start?.offset != null && + node?.position?.end?.offset != null + ) { + codeText = content.slice( + node.position.start.offset, + node.position.end.offset + ); + codeText = codeText.trim(); + + // Find the last occurrence of closing backticks + const lastBackticksIndex = codeText.lastIndexOf("```"); + if (lastBackticksIndex !== -1) { + codeText = codeText.slice(0, lastBackticksIndex + 3); + } + + // Remove the language declaration and trailing backticks + const codeLines = codeText.split("\n"); + if (codeLines.length > 1 && codeLines[0].trim().startsWith("```")) { + codeLines.shift(); // Remove the first line with the language declaration + if (codeLines[codeLines.length - 1]?.trim() === "```") { + codeLines.pop(); // Remove the last line with the trailing backticks + } + + const minIndent = codeLines + .filter((line) => line.trim().length > 0) + .reduce((min, line) => { + const match = line.match(/^\s*/); + return Math.min(min, match ? match[0].length : 0); + }, Infinity); + + const formattedCodeLines = codeLines.map((line) => line.slice(minIndent)); + codeText = formattedCodeLines.join("\n"); + } + } else { + // Fallback if position offsets are not available + codeText = children?.toString() || null; + } + + return codeText || ""; +} diff --git a/web/src/components/chat_search/MinimalMarkdown.tsx b/web/src/components/chat_search/MinimalMarkdown.tsx index 3516749d1bf..4731e2de9c3 100644 --- a/web/src/components/chat_search/MinimalMarkdown.tsx +++ b/web/src/components/chat_search/MinimalMarkdown.tsx @@ -1,4 +1,5 @@ import { CodeBlock } from "@/app/chat/message/CodeBlock"; +import { extractCodeText } from "@/app/chat/message/codeUtils"; import { MemoizedLink, MemoizedParagraph, @@ -10,13 +11,11 @@ import remarkGfm from "remark-gfm"; interface MinimalMarkdownProps { content: string; className?: string; - useCodeBlock?: boolean; } export const MinimalMarkdown: React.FC = ({ content, className = "", - useCodeBlock = false, }) => { return ( = ({ components={{ a: MemoizedLink, p: MemoizedParagraph, - code: useCodeBlock - ? (props) => ( - - ) - : (props) => , + code: ({ node, inline, className, children, ...props }: any) => { + const codeText = extractCodeText(node, content, children); + + return ( + + {children} + + ); + }, }} remarkPlugins={[remarkGfm]} > diff --git a/web/src/components/search/results/AnswerSection.tsx b/web/src/components/search/results/AnswerSection.tsx index 225623b0085..324e41e0a84 100644 --- a/web/src/components/search/results/AnswerSection.tsx +++ b/web/src/components/search/results/AnswerSection.tsx @@ -1,7 +1,5 @@ import { Quote } from "@/lib/search/interfaces"; import { ResponseSection, StatusOptions } from "./ResponseSection"; -import ReactMarkdown from "react-markdown"; -import remarkGfm from "remark-gfm"; import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown"; const TEMP_STRING = "__$%^TEMP$%^__"; @@ -40,12 +38,7 @@ export const AnswerSection = (props: AnswerSectionProps) => { status = "success"; header = <>; - body = ( - - ); + body = ; // error while building answer (NOTE: if error occurs during quote generation // the above if statement will hit and the error will not be displayed) @@ -61,9 +54,7 @@ export const AnswerSection = (props: AnswerSectionProps) => { } else if (props.answer) { status = "success"; header = <>; - body = ( - - ); + body = ; } return (