mirror of
https://github.com/danswer-ai/danswer.git
synced 2025-09-27 12:29:41 +02:00
Better virtualization (#2653)
This commit is contained in:
@@ -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();
|
||||||
|
@@ -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";
|
||||||
|
@@ -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";
|
||||||
|
@@ -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
|
||||||
|
47
web/src/app/chat/message/codeUtils.ts
Normal file
47
web/src/app/chat/message/codeUtils.ts
Normal 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 || "";
|
||||||
|
}
|
@@ -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]}
|
||||||
>
|
>
|
||||||
|
@@ -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 (
|
||||||
|
Reference in New Issue
Block a user