Memoize AI message component (#2483)

* memoize AI message component

* rename memoized file

* remove "zz"

* update name

* memoize for coverage

* add display name
This commit is contained in:
pablodanswer
2024-09-17 11:47:23 -07:00
committed by GitHub
parent c5032d25c9
commit cd58c96014
4 changed files with 141 additions and 149 deletions

View File

@@ -1,5 +1,4 @@
import React from "react"; import React, { useState, ReactNode, useCallback, useMemo, memo } from "react";
import { useState, ReactNode } from "react";
import { FiCheck, FiCopy } from "react-icons/fi"; import { FiCheck, FiCopy } from "react-icons/fi";
const CODE_BLOCK_PADDING_TYPE = { padding: "1rem" }; const CODE_BLOCK_PADDING_TYPE = { padding: "1rem" };
@@ -11,34 +10,23 @@ interface CodeBlockProps {
[key: string]: any; [key: string]: any;
} }
export function CodeBlock({ export const CodeBlock = memo(function CodeBlock({
className = "", className = "",
children, children,
content, content,
...props ...props
}: CodeBlockProps) { }: CodeBlockProps) {
const language = className const [copied, setCopied] = useState(false);
const language = useMemo(() => {
return className
.split(" ") .split(" ")
.filter((cls) => cls.startsWith("language-")) .filter((cls) => cls.startsWith("language-"))
.map((cls) => cls.replace("language-", "")) .map((cls) => cls.replace("language-", ""))
.join(" "); .join(" ");
const [copied, setCopied] = useState(false); }, [className]);
if (!language) {
// this is the case of a single "`" e.g. `hi`
if (typeof children === "string") {
return <code className={className}>{children}</code>;
}
return (
<pre style={CODE_BLOCK_PADDING_TYPE}>
<code {...props} className={`text-sm ${className}`}>
{children}
</code>
</pre>
);
}
const codeText = useMemo(() => {
let codeText: string | null = null; let codeText: string | null = null;
if ( if (
props.node?.position?.start?.offset && props.node?.position?.start?.offset &&
@@ -60,7 +48,8 @@ export function CodeBlock({
const codeLines = codeText.split("\n"); const codeLines = codeText.split("\n");
if ( if (
codeLines.length > 1 && codeLines.length > 1 &&
(codeLines[0].startsWith("```") || codeLines[0].trim().startsWith("```")) (codeLines[0].startsWith("```") ||
codeLines[0].trim().startsWith("```"))
) { ) {
codeLines.shift(); // Remove the first line with the language declaration codeLines.shift(); // Remove the first line with the language declaration
if ( if (
@@ -77,7 +66,9 @@ export function CodeBlock({
return Math.min(min, match ? match[0].length : 0); return Math.min(min, match ? match[0].length : 0);
}, Infinity); }, Infinity);
const formattedCodeLines = codeLines.map((line) => line.slice(minIndent)); const formattedCodeLines = codeLines.map((line) =>
line.slice(minIndent)
);
codeText = formattedCodeLines.join("\n"); codeText = formattedCodeLines.join("\n");
} }
} }
@@ -103,7 +94,11 @@ export function CodeBlock({
codeText = findTextNode(props.node); codeText = findTextNode(props.node);
} }
const handleCopy = (event: React.MouseEvent) => { return codeText;
}, [content, props.node]);
const handleCopy = useCallback(
(event: React.MouseEvent) => {
event.preventDefault(); event.preventDefault();
if (!codeText) { if (!codeText) {
return; return;
@@ -111,9 +106,25 @@ export function CodeBlock({
navigator.clipboard.writeText(codeText).then(() => { navigator.clipboard.writeText(codeText).then(() => {
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); // Reset copy status after 2 seconds setTimeout(() => setCopied(false), 2000);
}); });
}; },
[codeText]
);
if (!language) {
if (typeof children === "string") {
return <code className={className}>{children}</code>;
}
return (
<pre style={CODE_BLOCK_PADDING_TYPE}>
<code {...props} className={`text-sm ${className}`}>
{children}
</code>
</pre>
);
}
return ( return (
<div className="overflow-x-hidden"> <div className="overflow-x-hidden">
@@ -143,4 +154,4 @@ export function CodeBlock({
</pre> </pre>
</div> </div>
); );
} });

View File

@@ -0,0 +1,33 @@
import { Citation } from "@/components/search/results/Citation";
import React, { memo } from "react";
export const MemoizedLink = memo((props: any) => {
const { node, ...rest } = props;
const value = rest.children;
if (value?.toString().startsWith("*")) {
return (
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
);
} else if (value?.toString().startsWith("[")) {
return <Citation link={rest?.href}>{rest.children}</Citation>;
} else {
return (
<a
onMouseDown={() =>
rest.href ? window.open(rest.href, "_blank") : undefined
}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
}
});
export const MemoizedParagraph = memo(({ node, ...props }: any) => (
<p {...props} className="text-default" />
));
MemoizedLink.displayName = "MemoizedLink";
MemoizedParagraph.displayName = "MemoizedParagraph";

View File

@@ -8,14 +8,7 @@ import {
FiGlobe, FiGlobe,
} from "react-icons/fi"; } from "react-icons/fi";
import { FeedbackType } from "../types"; import { FeedbackType } from "../types";
import { import React, { useContext, useEffect, useMemo, useRef, useState } from "react";
Dispatch,
SetStateAction,
useContext,
useEffect,
useRef,
useState,
} from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import { import {
DanswerDocument, DanswerDocument,
@@ -46,12 +39,7 @@ import { AssistantIcon } from "@/components/assistants/AssistantIcon";
import { Citation } from "@/components/search/results/Citation"; import { Citation } from "@/components/search/results/Citation";
import { DocumentMetadataBlock } from "@/components/search/DocumentDisplay"; import { DocumentMetadataBlock } from "@/components/search/DocumentDisplay";
import { import { LikeFeedback, DislikeFeedback } from "@/components/icons/icons";
ThumbsUpIcon,
ThumbsDownIcon,
LikeFeedback,
DislikeFeedback,
} from "@/components/icons/icons";
import { import {
CustomTooltip, CustomTooltip,
TooltipGroup, TooltipGroup,
@@ -65,6 +53,7 @@ import GeneratingImageDisplay from "../tools/GeneratingImageDisplay";
import RegenerateOption from "../RegenerateOption"; import RegenerateOption from "../RegenerateOption";
import { LlmOverride } from "@/lib/hooks"; import { LlmOverride } from "@/lib/hooks";
import { ContinueGenerating } from "./ContinueMessage"; import { ContinueGenerating } from "./ContinueMessage";
import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents";
const TOOLS_WITH_CUSTOM_HANDLING = [ const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME, SEARCH_TOOL_NAME,
@@ -367,41 +356,8 @@ export const AIMessage = ({
key={messageId} key={messageId}
className="prose max-w-full text-base" className="prose max-w-full text-base"
components={{ components={{
a: (props) => { a: MemoizedLink,
const { node, ...rest } = props; p: MemoizedParagraph,
const value = rest.children;
if (value?.toString().startsWith("*")) {
return (
<div className="flex-none bg-background-800 inline-block rounded-full h-3 w-3 ml-2" />
);
} else if (
value?.toString().startsWith("[")
) {
// for some reason <a> tags cause the onClick to not apply
// and the links are unclickable
// TODO: fix the fact that you have to double click to follow link
// for the first link
return (
<Citation link={rest?.href}>
{rest.children}
</Citation>
);
} else {
return (
<a
onMouseDown={() =>
rest.href
? window.open(rest.href, "_blank")
: undefined
}
className="cursor-pointer text-link hover:text-link-hover"
>
{rest.children}
</a>
);
}
},
code: (props) => ( code: (props) => (
<CodeBlock <CodeBlock
className="w-full" className="w-full"
@@ -409,9 +365,6 @@ export const AIMessage = ({
content={content as string} content={content as string}
/> />
), ),
p: ({ node, ...props }) => (
<p {...props} className="text-default" />
),
}} }}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
rehypePlugins={[ rehypePlugins={[

View File

@@ -1,4 +1,8 @@
import { CodeBlock } from "@/app/chat/message/CodeBlock"; import { CodeBlock } from "@/app/chat/message/CodeBlock";
import {
MemoizedLink,
MemoizedParagraph,
} from "@/app/chat/message/MemoizedTextComponents";
import React from "react"; import React from "react";
import ReactMarkdown from "react-markdown"; import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm"; import remarkGfm from "remark-gfm";
@@ -18,17 +22,8 @@ export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
<ReactMarkdown <ReactMarkdown
className={`w-full text-wrap break-word ${className}`} className={`w-full text-wrap break-word ${className}`}
components={{ components={{
a: ({ node, ...props }) => ( a: MemoizedLink,
<a p: MemoizedParagraph,
{...props}
className="text-sm text-link hover:text-link-hover"
target="_blank"
rel="noopener noreferrer"
/>
),
p: ({ node, ...props }) => (
<p {...props} className="text-wrap break-word text-sm m-0 w-full" />
),
code: useCodeBlock code: useCodeBlock
? (props) => ( ? (props) => (
<CodeBlock className="w-full" {...props} content={content} /> <CodeBlock className="w-full" {...props} content={content} />