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(() => ( +
+ {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 (