fix code block cannot be selected while generating
This commit is contained in:
parent
d9959cb7af
commit
1dc99ef775
4 changed files with 84 additions and 41 deletions
Binary file not shown.
|
@ -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>
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue