fix code block cannot be selected while generating

This commit is contained in:
Xuan Son Nguyen 2025-02-06 17:08:04 +01:00
parent d9959cb7af
commit 1dc99ef775
4 changed files with 84 additions and 41 deletions

Binary file not shown.

View file

@ -2,7 +2,7 @@ import { useMemo, useState } from 'react';
import { useAppContext } from '../utils/app.context';
import { Message, PendingMessage } from '../utils/types';
import { classNames } from '../utils/misc';
import MarkdownDisplay from './MarkdownDisplay';
import MarkdownDisplay, { CopyButton } from './MarkdownDisplay';
interface SplitMessage {
content: PendingMessage['content'];
@ -207,20 +207,19 @@ export default function ChatMessage({
{/* assistant message */}
{msg.role === 'assistant' && (
<>
<button
{!isPending && (
<button
className="badge btn-mini show-on-hover mr-2"
onClick={regenerate}
disabled={msg.content === null}
>
🔄 Regenerate
</button>
)}
<CopyButton
className="badge btn-mini show-on-hover mr-2"
onClick={regenerate}
disabled={msg.content === null}
>
🔄 Regenerate
</button>
<button
className="badge btn-mini show-on-hover mr-2"
onClick={() => navigator.clipboard.writeText(msg.content || '')}
disabled={msg.content === null}
>
📋 Copy
</button>
content={msg.content}
/>
</>
)}
</div>

View file

@ -27,7 +27,7 @@ export default function ChatScreen() {
msgListElem.scrollHeight -
msgListElem.scrollTop -
msgListElem.clientHeight;
if (!requiresNearBottom || spaceToBottom < 100) {
if (!requiresNearBottom || spaceToBottom < 50) {
setTimeout(
() => msgListElem.scrollTo({ top: msgListElem.scrollHeight }),
1

View file

@ -6,7 +6,9 @@ import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks';
import 'katex/dist/katex.min.css';
import { copyStr } from '../utils/misc';
import { classNames, copyStr } from '../utils/misc';
import { ElementContent, Root } from 'hast';
import { visit } from 'unist-util-visit';
export default function MarkdownDisplay({ content }: { content: string }) {
const preprocessedContent = useMemo(
@ -16,9 +18,11 @@ export default function MarkdownDisplay({ content }: { content: string }) {
return (
<Markdown
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
rehypePlugins={[rehypeHightlight, rehypeKatex]}
rehypePlugins={[rehypeHightlight, rehypeKatex, rehypeCustomCopyButton]}
components={{
pre: (props) => <Pre {...props} origContent={preprocessedContent} />,
button: (props) => (
<CopyCodeButton {...props} origContent={preprocessedContent} />
),
}}
>
{preprocessedContent}
@ -26,15 +30,14 @@ export default function MarkdownDisplay({ content }: { content: string }) {
);
}
const Pre: React.ElementType<
React.ClassAttributes<HTMLPreElement> &
React.HTMLAttributes<HTMLPreElement> &
const CopyCodeButton: React.ElementType<
React.ClassAttributes<HTMLButtonElement> &
React.HTMLAttributes<HTMLButtonElement> &
ExtraProps & { origContent: string }
> = ({ node, origContent, ...props }) => {
> = ({ node, origContent }) => {
const startOffset = node?.position?.start.offset ?? 0;
const endOffset = node?.position?.end.offset ?? 0;
const [copied, setCopied] = useState(false);
const copiedContent = useMemo(
() =>
origContent
@ -44,29 +47,70 @@ const Pre: React.ElementType<
[origContent, startOffset, endOffset]
);
if (!node?.position) {
return <pre {...props} />;
}
return (
<div className="relative my-4">
<div
className="text-right sticky top-4 mb-2 mr-2 h-0"
onClick={() => {
copyStr(copiedContent);
setCopied(true);
}}
onMouseLeave={() => setCopied(false)}
>
<button className="badge btn-mini">
{copied ? 'Copied!' : '📋 Copy'}
</button>
</div>
<pre {...props} />
<div
className={classNames({
'text-right sticky top-4 mb-2 mr-2 h-0': true,
'display-none': !node?.position,
})}
>
<CopyButton className="badge btn-mini" content={copiedContent} />
</div>
);
};
export const CopyButton = ({
content,
className,
}: {
content: string;
className?: string;
}) => {
const [copied, setCopied] = useState(false);
return (
<button
className={className}
onClick={() => {
copyStr(content);
setCopied(true);
}}
onMouseLeave={() => setCopied(false)}
>
{copied ? 'Copied!' : '📋 Copy'}
</button>
);
};
/**
* This injects the "button" element before each "pre" element.
* The actual button will be replaced with a react component in the MarkdownDisplay.
* We don't replace "pre" node directly because it will cause the node to re-render, which causes this bug: https://github.com/ggerganov/llama.cpp/issues/9608
*/
function rehypeCustomCopyButton() {
return function (tree: Root) {
visit(tree, 'element', function (node) {
if (node.tagName === 'pre' && !node.properties.visited) {
const preNode = { ...node };
// replace current node
preNode.properties.visited = 'true';
node.tagName = 'div';
node.properties = {
className: 'relative my-4',
};
// add node for button
const btnNode: ElementContent = {
type: 'element',
tagName: 'button',
properties: {},
children: [],
position: node.position,
};
node.children = [btnNode, preNode];
}
});
};
}
/**
* The part below is copied and adapted from:
* https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts