Skip to content

Commit b4fc366

Browse files
committed
copy button for markdown
1 parent 57882c5 commit b4fc366

File tree

3 files changed

+91
-143
lines changed

3 files changed

+91
-143
lines changed

src/chat/Message.tsx

+9-143
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@
22
import { Box, Paper, Typography, IconButton, Button } from "@mui/material";
33
import UndoIcon from "@mui/icons-material/Undo";
44
import { FunctionComponent, PropsWithChildren, useState } from "react";
5-
import ReactMarkdown from "react-markdown";
6-
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
7-
import remarkGfm from "remark-gfm";
8-
import { vs as highlightStyle } from "react-syntax-highlighter/dist/esm/styles/prism";
5+
import MarkdownContent from "../components/MarkdownContent";
96
import { ORMessage } from "../pages/HomePage/openRouterTypes";
107

118
type MessageContainerProps = {
@@ -141,23 +138,9 @@ const Message: FunctionComponent<MessageProps> = ({
141138
message.tool_calls.map((toolCall) => (
142139
<Box key={toolCall.id} sx={{ mb: 1 }}>
143140
{toolCall.function.name === "execute_python_code" ? (
144-
<ReactMarkdown
145-
remarkPlugins={[remarkGfm]}
146-
components={{
147-
code({ children }) {
148-
return (
149-
<SyntaxHighlighter
150-
PreTag="div"
151-
children={String(children).replace(/\n$/, "")}
152-
language="python"
153-
style={highlightStyle}
154-
/>
155-
);
156-
},
157-
}}
158-
>
159-
{`\`\`\`python\n${JSON.parse(toolCall.function.arguments).code}\n\`\`\``}
160-
</ReactMarkdown>
141+
<MarkdownContent
142+
content={`\`\`\`python\n${JSON.parse(toolCall.function.arguments).code}\n\`\`\``}
143+
/>
161144
) : (
162145
<Typography
163146
variant="body2"
@@ -200,45 +183,9 @@ const Message: FunctionComponent<MessageProps> = ({
200183
</Box>
201184
{toolResultExpanded && (
202185
<Box>
203-
<ReactMarkdown
204-
remarkPlugins={[remarkGfm]}
205-
components={{
206-
a({ children, ...props }) {
207-
return (
208-
<a
209-
href={props.href}
210-
target="_blank"
211-
rel="noopener noreferrer"
212-
{...props}
213-
>
214-
{children}
215-
</a>
216-
);
217-
},
218-
code(props) {
219-
const { children, className, ...rest } = props;
220-
const match = /language-(\w+)/.exec(className || "");
221-
return match ? (
222-
<SyntaxHighlighter
223-
PreTag="div"
224-
children={String(children).replace(/\n$/, "")}
225-
language={match[1]}
226-
style={highlightStyle}
227-
/>
228-
) : (
229-
<code
230-
{...rest}
231-
className={className}
232-
style={{ background: "#eee" }}
233-
>
234-
{children}
235-
</code>
236-
);
237-
},
238-
}}
239-
>
240-
{formatMessageContent(message.content)}
241-
</ReactMarkdown>
186+
<MarkdownContent
187+
content={formatMessageContent(message.content)}
188+
/>
242189
</Box>
243190
)}
244191
</Box>
@@ -247,95 +194,14 @@ const Message: FunctionComponent<MessageProps> = ({
247194

248195
// Handle regular text content
249196
if (typeof message.content === "string") {
250-
return (
251-
<ReactMarkdown
252-
remarkPlugins={[remarkGfm]}
253-
components={{
254-
a({ children, ...props }) {
255-
return (
256-
<a
257-
href={props.href}
258-
target="_blank"
259-
rel="noopener noreferrer"
260-
{...props}
261-
>
262-
{children}
263-
</a>
264-
);
265-
},
266-
code(props) {
267-
const { children, className, ...rest } = props;
268-
const match = /language-(\w+)/.exec(className || "");
269-
return match ? (
270-
<SyntaxHighlighter
271-
PreTag="div"
272-
children={String(children).replace(/\n$/, "")}
273-
language={match[1]}
274-
style={highlightStyle}
275-
/>
276-
) : (
277-
<code
278-
{...rest}
279-
className={className}
280-
style={{ background: "#eee" }}
281-
>
282-
{children}
283-
</code>
284-
);
285-
},
286-
}}
287-
>
288-
{message.content}
289-
</ReactMarkdown>
290-
);
197+
return <MarkdownContent content={message.content} />;
291198
}
292199

293200
// Handle array of content parts (e.g. text + images)
294201
if (Array.isArray(message.content)) {
295202
return message.content.map((part, index) => {
296203
if (part.type === "text") {
297-
return (
298-
<ReactMarkdown
299-
key={index}
300-
remarkPlugins={[remarkGfm]}
301-
components={{
302-
a({ children, ...props }) {
303-
return (
304-
<a
305-
href={props.href}
306-
target="_blank"
307-
rel="noopener noreferrer"
308-
{...props}
309-
>
310-
{children}
311-
</a>
312-
);
313-
},
314-
code(props) {
315-
const { children, className, ...rest } = props;
316-
const match = /language-(\w+)/.exec(className || "");
317-
return match ? (
318-
<SyntaxHighlighter
319-
PreTag="div"
320-
children={String(children).replace(/\n$/, "")}
321-
language={match[1]}
322-
style={highlightStyle}
323-
/>
324-
) : (
325-
<code
326-
{...rest}
327-
className={className}
328-
style={{ background: "#eee" }}
329-
>
330-
{children}
331-
</code>
332-
);
333-
},
334-
}}
335-
>
336-
{part.text}
337-
</ReactMarkdown>
338-
);
204+
return <MarkdownContent key={index} content={part.text} />;
339205
}
340206
if (part.type === "image_url") {
341207
return (

src/components/CodeCellEditor.tsx

+1
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ const CodeCellEditor: FunctionComponent<CodeCellEditorProps> = ({
6565
editor.onKeyDown((event) => {
6666
// do not propagate special key events such as "a" and "b"
6767
if (
68+
!event.ctrlKey &&
6869
[
6970
monaco.KeyCode.KeyA,
7071
monaco.KeyCode.KeyB,

src/components/MarkdownContent.tsx

+81
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
import { FunctionComponent } from "react";
2+
import ReactMarkdown from "react-markdown";
3+
import remarkGfm from "remark-gfm";
4+
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
5+
import { vs as highlightStyle } from "react-syntax-highlighter/dist/esm/styles/prism";
6+
7+
interface MarkdownContentProps {
8+
content: string;
9+
}
10+
11+
const MarkdownContent: FunctionComponent<MarkdownContentProps> = ({
12+
content,
13+
}) => {
14+
return (
15+
<ReactMarkdown
16+
remarkPlugins={[remarkGfm]}
17+
components={{
18+
a({ children, ...props }) {
19+
return (
20+
<a
21+
href={props.href}
22+
target="_blank"
23+
rel="noopener noreferrer"
24+
{...props}
25+
>
26+
{children}
27+
</a>
28+
);
29+
},
30+
code(props) {
31+
const { children, className, ...rest } = props;
32+
const match = /language-(\w+)/.exec(className || "");
33+
const code = String(children).replace(/\n$/, "");
34+
35+
const handleCopy = () => {
36+
navigator.clipboard.writeText(code);
37+
};
38+
39+
return match ? (
40+
<div style={{ position: "relative" }}>
41+
<SyntaxHighlighter
42+
PreTag="div"
43+
children={code}
44+
language={match[1]}
45+
style={highlightStyle}
46+
/>
47+
<button
48+
onClick={handleCopy}
49+
style={{
50+
position: "absolute",
51+
right: "8px",
52+
bottom: "24px",
53+
padding: "4px 8px",
54+
fontSize: "12px",
55+
background: "#f5f5f5",
56+
border: "1px solid #ddd",
57+
borderRadius: "4px",
58+
cursor: "pointer",
59+
}}
60+
>
61+
Copy
62+
</button>
63+
</div>
64+
) : (
65+
<code
66+
{...rest}
67+
className={className}
68+
style={{ background: "#eee" }}
69+
>
70+
{children}
71+
</code>
72+
);
73+
},
74+
}}
75+
>
76+
{content}
77+
</ReactMarkdown>
78+
);
79+
};
80+
81+
export default MarkdownContent;

0 commit comments

Comments
 (0)