File size: 5,278 Bytes
12621bc 136f9cf 12621bc |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 |
import React from "react";
import { Button } from "@/components/ui/button";
import { ClipboardCopy } from "lucide-react";
import { toast } from "sonner";
import { MessageProps } from "../types";
import ReactMarkdown from "react-markdown";
import { Prism as SyntaxHighlighter } from "react-syntax-highlighter";
import { oneDark } from "react-syntax-highlighter/dist/esm/styles/prism";
import remarkMath from "remark-math";
import rehypeKatex from "rehype-katex";
import remarkGfm from "remark-gfm";
import { Badge } from "@/components/ui/badge";
import "katex/dist/katex.min.css";
export const AIMessageComponent = React.memo(({ message }: MessageProps) => {
const handleCopy = React.useCallback(() => {
const content = String(message.content);
navigator.clipboard.writeText(content)
.then(() => toast.success("Response copied to clipboard"))
.catch(() => toast.error("Failed to copy response"));
}, [message.content]);
return (
<div className="flex flex-col gap-1 group">
<div className="prose prose-sm dark:prose-invert max-w-none">
<ReactMarkdown
remarkPlugins={[remarkMath, remarkGfm]}
rehypePlugins={[rehypeKatex]}
components={{
p(props) {
return <p {...props} className="leading-7 mb-4" />;
},
h1(props) {
return <h1 {...props} className="text-3xl font-bold tracking-tight mb-4 mt-8" />;
},
h2(props) {
return <h2 {...props} className="text-2xl font-semibold tracking-tight mb-4 mt-8" />;
},
h3(props) {
return <h3 {...props} className="text-xl font-semibold tracking-tight mb-4 mt-6" />;
},
h4(props) {
return <h4 {...props} className="text-lg font-semibold tracking-tight mb-4 mt-6" />;
},
code(props) {
const {children, className, ...rest} = props;
const match = /language-(\w+)/.exec(className || '');
const language = match ? match[1] : '';
const code = String(children).replace(/\n$/, '');
const copyToClipboard = () => {
navigator.clipboard.writeText(code);
toast.success("Code copied to clipboard");
};
return match ? (
<div className="relative rounded-md overflow-hidden my-6">
<div className="absolute right-2 top-2 flex items-center gap-2">
{language && (
<Badge variant="secondary" className="text-xs font-mono">
{language}
</Badge>
)}
<Button
variant="ghost"
size="icon"
className="h-6 w-6 bg-muted/50 hover:bg-muted"
onClick={copyToClipboard}
>
<ClipboardCopy className="h-3 w-3" />
</Button>
</div>
<SyntaxHighlighter
style={oneDark}
language={language}
PreTag="div"
customStyle={{ margin: 0, borderRadius: 0, padding: "1.5rem" }}
>
{code}
</SyntaxHighlighter>
</div>
) : (
<code {...rest} className={`${className} bg-muted px-1.5 py-0.5 rounded-md text-sm`}>
{children}
</code>
);
},
a(props) {
return <a {...props} className="text-primary hover:underline font-medium" target="_blank" rel="noopener noreferrer" />;
},
table(props) {
return <div className="my-6 w-full overflow-y-auto"><table {...props} className="w-full border-collapse table-auto" /></div>;
},
th(props) {
return <th {...props} className="border border-muted-foreground px-4 py-2 text-left font-semibold" />;
},
td(props) {
return <td {...props} className="border border-muted-foreground px-4 py-2" />;
},
blockquote(props) {
return <blockquote {...props} className="mt-6 border-l-4 border-primary pl-6 italic" />;
},
ul(props) {
return <ul {...props} className="my-6 ml-6 list-disc [&>li]:mt-2" />;
},
ol(props) {
return <ol {...props} className="my-6 ml-6 list-decimal [&>li]:mt-2" />;
},
li(props) {
return <li {...props} className="leading-7" />;
},
hr(props) {
return <hr {...props} className="my-6 border-muted" />;
}
}}
>
{String(message.content)}
</ReactMarkdown>
</div>
<div className="flex flex-row gap-1 opacity-0 group-hover:opacity-100">
<Button variant="ghost" size="sm" onClick={handleCopy}>
<ClipboardCopy className="h-4 w-4 mr-2" />
Copy response
</Button>
</div>
</div>
);
});
AIMessageComponent.displayName = "AIMessageComponent"; |