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";