@@ -6,7 +6,9 @@ import rehypeKatex from 'rehype-katex';
66import remarkMath from 'remark-math' ;
77import remarkBreaks from 'remark-breaks' ;
88import 'katex/dist/katex.min.css' ;
9- import { copyStr } from '../utils/misc' ;
9+ import { classNames , copyStr } from '../utils/misc' ;
10+ import { ElementContent , Root } from 'hast' ;
11+ import { visit } from 'unist-util-visit' ;
1012
1113export default function MarkdownDisplay ( { content } : { content : string } ) {
1214 const preprocessedContent = useMemo (
@@ -16,25 +18,26 @@ export default function MarkdownDisplay({ content }: { content: string }) {
1618 return (
1719 < Markdown
1820 remarkPlugins = { [ remarkGfm , remarkMath , remarkBreaks ] }
19- rehypePlugins = { [ rehypeHightlight , rehypeKatex ] }
21+ rehypePlugins = { [ rehypeHightlight , rehypeKatex , rehypeCustomCopyButton ] }
2022 components = { {
21- pre : ( props ) => < Pre { ...props } origContent = { preprocessedContent } /> ,
23+ button : ( props ) => (
24+ < CopyCodeButton { ...props } origContent = { preprocessedContent } />
25+ ) ,
2226 } }
2327 >
2428 { preprocessedContent }
2529 </ Markdown >
2630 ) ;
2731}
2832
29- const Pre : React . ElementType <
30- React . ClassAttributes < HTMLPreElement > &
31- React . HTMLAttributes < HTMLPreElement > &
33+ const CopyCodeButton : React . ElementType <
34+ React . ClassAttributes < HTMLButtonElement > &
35+ React . HTMLAttributes < HTMLButtonElement > &
3236 ExtraProps & { origContent : string }
33- > = ( { node, origContent, ... props } ) => {
37+ > = ( { node, origContent } ) => {
3438 const startOffset = node ?. position ?. start . offset ?? 0 ;
3539 const endOffset = node ?. position ?. end . offset ?? 0 ;
3640
37- const [ copied , setCopied ] = useState ( false ) ;
3841 const copiedContent = useMemo (
3942 ( ) =>
4043 origContent
@@ -44,29 +47,70 @@ const Pre: React.ElementType<
4447 [ origContent , startOffset , endOffset ]
4548 ) ;
4649
47- if ( ! node ?. position ) {
48- return < pre { ...props } /> ;
49- }
50-
5150 return (
52- < div className = "relative my-4" >
53- < div
54- className = "text-right sticky top-4 mb-2 mr-2 h-0"
55- onClick = { ( ) => {
56- copyStr ( copiedContent ) ;
57- setCopied ( true ) ;
58- } }
59- onMouseLeave = { ( ) => setCopied ( false ) }
60- >
61- < button className = "badge btn-mini" >
62- { copied ? 'Copied!' : '📋 Copy' }
63- </ button >
64- </ div >
65- < pre { ...props } />
51+ < div
52+ className = { classNames ( {
53+ 'text-right sticky top-4 mb-2 mr-2 h-0' : true ,
54+ 'display-none' : ! node ?. position ,
55+ } ) }
56+ >
57+ < CopyButton className = "badge btn-mini" content = { copiedContent } />
6658 </ div >
6759 ) ;
6860} ;
6961
62+ export const CopyButton = ( {
63+ content,
64+ className,
65+ } : {
66+ content : string ;
67+ className ?: string ;
68+ } ) => {
69+ const [ copied , setCopied ] = useState ( false ) ;
70+ return (
71+ < button
72+ className = { className }
73+ onClick = { ( ) => {
74+ copyStr ( content ) ;
75+ setCopied ( true ) ;
76+ } }
77+ onMouseLeave = { ( ) => setCopied ( false ) }
78+ >
79+ { copied ? 'Copied!' : '📋 Copy' }
80+ </ button >
81+ ) ;
82+ } ;
83+
84+ /**
85+ * This injects the "button" element before each "pre" element.
86+ * The actual button will be replaced with a react component in the MarkdownDisplay.
87+ * 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
88+ */
89+ function rehypeCustomCopyButton ( ) {
90+ return function ( tree : Root ) {
91+ visit ( tree , 'element' , function ( node ) {
92+ if ( node . tagName === 'pre' && ! node . properties . visited ) {
93+ const preNode = { ...node } ;
94+ // replace current node
95+ preNode . properties . visited = 'true' ;
96+ node . tagName = 'div' ;
97+ node . properties = {
98+ className : 'relative my-4' ,
99+ } ;
100+ // add node for button
101+ const btnNode : ElementContent = {
102+ type : 'element' ,
103+ tagName : 'button' ,
104+ properties : { } ,
105+ children : [ ] ,
106+ position : node . position ,
107+ } ;
108+ node . children = [ btnNode , preNode ] ;
109+ }
110+ } ) ;
111+ } ;
112+ }
113+
70114/**
71115 * The part below is copied and adapted from:
72116 * https://github.com/danny-avila/LibreChat/blob/main/client/src/utils/latex.ts
0 commit comments