Better virtualization (#2653)

This commit is contained in:
pablodanswer
2024-10-02 11:14:59 -07:00
committed by GitHub
parent a0235b7b7b
commit af187c6cfe
7 changed files with 198 additions and 163 deletions

View File

@@ -759,7 +759,15 @@ export function ChatPage({
setAboveHorizon(scrollDist.current > 500); 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 = () => { const handleInputResize = () => {
setTimeout(() => { setTimeout(() => {
@@ -1137,7 +1145,9 @@ export function ChatPage({
await delay(50); await delay(50);
while (!stack.isComplete || !stack.isEmpty()) { while (!stack.isComplete || !stack.isEmpty()) {
if (stack.isEmpty()) {
await delay(0.5); await delay(0.5);
}
if (!stack.isEmpty() && !controller.signal.aborted) { if (!stack.isEmpty() && !controller.signal.aborted) {
const packet = stack.nextPacket(); const packet = stack.nextPacket();

View File

@@ -1,20 +1,22 @@
import React, { useState, ReactNode, useCallback, useMemo, memo } from "react"; import React, { useState, ReactNode, useCallback, useMemo, memo } 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 = { padding: "1rem" };
interface CodeBlockProps { interface CodeBlockProps {
className?: string | undefined; className?: string;
children?: ReactNode; children?: ReactNode;
content: string; codeText: string;
[key: string]: any;
} }
const MemoizedCodeLine = memo(({ content }: { content: ReactNode }) => (
<>{content}</>
));
export const CodeBlock = memo(function CodeBlock({ export const CodeBlock = memo(function CodeBlock({
className = "", className = "",
children, children,
content, codeText,
...props
}: CodeBlockProps) { }: CodeBlockProps) {
const [copied, setCopied] = useState(false); const [copied, setCopied] = useState(false);
@@ -26,111 +28,15 @@ export const CodeBlock = memo(function CodeBlock({
.join(" "); .join(" ");
}, [className]); }, [className]);
const codeText = useMemo(() => { const handleCopy = useCallback(() => {
let codeText: string | null = null; if (!codeText) return;
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)
);
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(() => { navigator.clipboard.writeText(codeText).then(() => {
setCopied(true); setCopied(true);
setTimeout(() => setCopied(false), 2000); setTimeout(() => setCopied(false), 2000);
}); });
}, }, [codeText]);
[codeText]
);
if (!language) { const CopyButton = memo(() => (
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 (
<div className="overflow-x-hidden">
<div className="flex mx-3 py-2 text-xs">
{language}
{codeText && (
<div <div
className="ml-auto cursor-pointer select-none" className="ml-auto cursor-pointer select-none"
onMouseDown={handleCopy} onMouseDown={handleCopy}
@@ -147,11 +53,74 @@ export const CodeBlock = memo(function CodeBlock({
</div> </div>
)} )}
</div> </div>
)} ));
</div> CopyButton.displayName = "CopyButton";
<pre {...props} className="overflow-x-scroll" style={{ padding: "1rem" }}>
<code className={`text-xs overflow-x-auto `}>{children}</code> const CodeContent = memo(() => {
if (!language) {
if (typeof children === "string") {
return (
<code
className={`
font-mono
text-gray-800
bg-gray-50
border
border-gray-300
rounded
px-1
py-[3px]
text-xs
whitespace-pre-wrap
break-words
overflow-hidden
mb-1
${className}
`}
>
{children}
</code>
);
}
return (
<pre style={CODE_BLOCK_PADDING}>
<code className={`text-sm ${className}`}>
{Array.isArray(children)
? children.map((child, index) => (
<MemoizedCodeLine key={index} content={child} />
))
: children}
</code>
</pre> </pre>
);
}
return (
<pre className="overflow-x-scroll" style={CODE_BLOCK_PADDING}>
<code className="text-xs overflow-x-auto">
{Array.isArray(children)
? children.map((child, index) => (
<MemoizedCodeLine key={index} content={child} />
))
: children}
</code>
</pre>
);
});
CodeContent.displayName = "CodeContent";
return (
<div className="overflow-x-hidden">
{language && (
<div className="flex mx-3 py-2 text-xs">
{language}
{codeText && <CopyButton />}
</div>
)}
<CodeContent />
</div> </div>
); );
}); });
CodeBlock.displayName = "CodeBlock";
MemoizedCodeLine.displayName = "MemoizedCodeLine";

View File

@@ -25,9 +25,9 @@ export const MemoizedLink = memo((props: any) => {
} }
}); });
export const MemoizedParagraph = memo(({ node, ...props }: any) => ( export const MemoizedParagraph = memo(({ ...props }: any) => {
<p {...props} className="text-default" /> return <p {...props} className="text-default" />;
)); });
MemoizedLink.displayName = "MemoizedLink"; MemoizedLink.displayName = "MemoizedLink";
MemoizedParagraph.displayName = "MemoizedParagraph"; MemoizedParagraph.displayName = "MemoizedParagraph";

View File

@@ -54,6 +54,7 @@ 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"; import { MemoizedLink, MemoizedParagraph } from "./MemoizedTextComponents";
import { extractCodeText } from "./codeUtils";
const TOOLS_WITH_CUSTOM_HANDLING = [ const TOOLS_WITH_CUSTOM_HANDLING = [
SEARCH_TOOL_NAME, SEARCH_TOOL_NAME,
@@ -253,6 +254,40 @@ export const AIMessage = ({
new Set((docs || []).map((doc) => doc.source_type)) new Set((docs || []).map((doc) => doc.source_type))
).slice(0, 3); ).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 (
<CodeBlock className={className} codeText={codeText}>
{children}
</CodeBlock>
);
},
}),
[messageId, content]
);
const renderedMarkdown = useMemo(() => {
return (
<ReactMarkdown
className="prose max-w-full text-base"
components={markdownComponents}
remarkPlugins={[remarkGfm]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }]]}
>
{finalContent as string}
</ReactMarkdown>
);
}, [finalContent]);
const includeMessageSwitcher = const includeMessageSwitcher =
currentMessageInd !== undefined && currentMessageInd !== undefined &&
onMessageSelection && onMessageSelection &&
@@ -352,27 +387,7 @@ export const AIMessage = ({
{typeof content === "string" ? ( {typeof content === "string" ? (
<div className="overflow-x-visible max-w-content-max"> <div className="overflow-x-visible max-w-content-max">
<ReactMarkdown {renderedMarkdown}
key={messageId}
className="prose max-w-full text-base"
components={{
a: MemoizedLink,
p: MemoizedParagraph,
code: (props) => (
<CodeBlock
className="w-full"
{...props}
content={content as string}
/>
),
}}
remarkPlugins={[remarkGfm]}
rehypePlugins={[
[rehypePrism, { ignoreMissing: true }],
]}
>
{finalContent as string}
</ReactMarkdown>
</div> </div>
) : ( ) : (
content content

View File

@@ -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 || "";
}

View File

@@ -1,4 +1,5 @@
import { CodeBlock } from "@/app/chat/message/CodeBlock"; import { CodeBlock } from "@/app/chat/message/CodeBlock";
import { extractCodeText } from "@/app/chat/message/codeUtils";
import { import {
MemoizedLink, MemoizedLink,
MemoizedParagraph, MemoizedParagraph,
@@ -10,13 +11,11 @@ import remarkGfm from "remark-gfm";
interface MinimalMarkdownProps { interface MinimalMarkdownProps {
content: string; content: string;
className?: string; className?: string;
useCodeBlock?: boolean;
} }
export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({ export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
content, content,
className = "", className = "",
useCodeBlock = false,
}) => { }) => {
return ( return (
<ReactMarkdown <ReactMarkdown
@@ -24,11 +23,15 @@ export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
components={{ components={{
a: MemoizedLink, a: MemoizedLink,
p: MemoizedParagraph, p: MemoizedParagraph,
code: useCodeBlock code: ({ node, inline, className, children, ...props }: any) => {
? (props) => ( const codeText = extractCodeText(node, content, children);
<CodeBlock className="w-full" {...props} content={content} />
) return (
: (props) => <code {...props} />, <CodeBlock className={className} codeText={codeText}>
{children}
</CodeBlock>
);
},
}} }}
remarkPlugins={[remarkGfm]} remarkPlugins={[remarkGfm]}
> >

View File

@@ -1,7 +1,5 @@
import { Quote } from "@/lib/search/interfaces"; import { Quote } from "@/lib/search/interfaces";
import { ResponseSection, StatusOptions } from "./ResponseSection"; import { ResponseSection, StatusOptions } from "./ResponseSection";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown"; import { MinimalMarkdown } from "@/components/chat_search/MinimalMarkdown";
const TEMP_STRING = "__$%^TEMP$%^__"; const TEMP_STRING = "__$%^TEMP$%^__";
@@ -40,12 +38,7 @@ export const AnswerSection = (props: AnswerSectionProps) => {
status = "success"; status = "success";
header = <></>; header = <></>;
body = ( body = <MinimalMarkdown content={replaceNewlines(props.answer || "")} />;
<MinimalMarkdown
useCodeBlock
content={replaceNewlines(props.answer || "")}
/>
);
// error while building answer (NOTE: if error occurs during quote generation // error while building answer (NOTE: if error occurs during quote generation
// the above if statement will hit and the error will not be displayed) // 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) { } else if (props.answer) {
status = "success"; status = "success";
header = <></>; header = <></>;
body = ( body = <MinimalMarkdown content={replaceNewlines(props.answer)} />;
<MinimalMarkdown useCodeBlock content={replaceNewlines(props.answer)} />
);
} }
return ( return (