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 { useAppContext } from '../utils/app.context';
import { Message, PendingMessage } from '../utils/types'; import { Message, PendingMessage } from '../utils/types';
import { classNames } from '../utils/misc'; import { classNames } from '../utils/misc';
import MarkdownDisplay from './MarkdownDisplay'; import MarkdownDisplay, { CopyButton } from './MarkdownDisplay';
interface SplitMessage { interface SplitMessage {
content: PendingMessage['content']; content: PendingMessage['content'];
@ -207,20 +207,19 @@ export default function ChatMessage({
{/* assistant message */} {/* assistant message */}
{msg.role === 'assistant' && ( {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" className="badge btn-mini show-on-hover mr-2"
onClick={regenerate} content={msg.content}
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>
</> </>
)} )}
</div> </div>

View file

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

View file

@ -6,7 +6,9 @@ import rehypeKatex from 'rehype-katex';
import remarkMath from 'remark-math'; import remarkMath from 'remark-math';
import remarkBreaks from 'remark-breaks'; import remarkBreaks from 'remark-breaks';
import 'katex/dist/katex.min.css'; 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 }) { export default function MarkdownDisplay({ content }: { content: string }) {
const preprocessedContent = useMemo( const preprocessedContent = useMemo(
@ -16,9 +18,11 @@ export default function MarkdownDisplay({ content }: { content: string }) {
return ( return (
<Markdown <Markdown
remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]} remarkPlugins={[remarkGfm, remarkMath, remarkBreaks]}
rehypePlugins={[rehypeHightlight, rehypeKatex]} rehypePlugins={[rehypeHightlight, rehypeKatex, rehypeCustomCopyButton]}
components={{ components={{
pre: (props) => <Pre {...props} origContent={preprocessedContent} />, button: (props) => (
<CopyCodeButton {...props} origContent={preprocessedContent} />
),
}} }}
> >
{preprocessedContent} {preprocessedContent}
@ -26,15 +30,14 @@ export default function MarkdownDisplay({ content }: { content: string }) {
); );
} }
const Pre: React.ElementType< const CopyCodeButton: React.ElementType<
React.ClassAttributes<HTMLPreElement> & React.ClassAttributes<HTMLButtonElement> &
React.HTMLAttributes<HTMLPreElement> & React.HTMLAttributes<HTMLButtonElement> &
ExtraProps & { origContent: string } ExtraProps & { origContent: string }
> = ({ node, origContent, ...props }) => { > = ({ node, origContent }) => {
const startOffset = node?.position?.start.offset ?? 0; const startOffset = node?.position?.start.offset ?? 0;
const endOffset = node?.position?.end.offset ?? 0; const endOffset = node?.position?.end.offset ?? 0;
const [copied, setCopied] = useState(false);
const copiedContent = useMemo( const copiedContent = useMemo(
() => () =>
origContent origContent
@ -44,29 +47,70 @@ const Pre: React.ElementType<
[origContent, startOffset, endOffset] [origContent, startOffset, endOffset]
); );
if (!node?.position) {
return <pre {...props} />;
}
return ( return (
<div className="relative my-4"> <div
<div className={classNames({
className="text-right sticky top-4 mb-2 mr-2 h-0" 'text-right sticky top-4 mb-2 mr-2 h-0': true,
onClick={() => { 'display-none': !node?.position,
copyStr(copiedContent); })}
setCopied(true); >
}} <CopyButton className="badge btn-mini" content={copiedContent} />
onMouseLeave={() => setCopied(false)}
>
<button className="badge btn-mini">
{copied ? 'Copied!' : '📋 Copy'}
</button>
</div>
<pre {...props} />
</div> </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: * The part below is copied and adapted from:
* https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts * https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts