File size: 7,746 Bytes
bb17288
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
import React, { useState, useEffect } from 'react';
import { motion } from 'framer-motion';
import { User, Copy, Check } from 'lucide-react';
import ReactMarkdown from 'react-markdown';
import remarkGfm from 'remark-gfm';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/esm/styles/prism';

export interface MessageProps {
    id: string;
    role: 'user' | 'bot';
    content: string;
    isNew?: boolean;
}

const CodeBlock = ({ inline, className, children, ...props }: { inline?: boolean; className?: string; children?: React.ReactNode } & React.HTMLAttributes<HTMLElement>) => {
    const match = /language-(\w+)/.exec(className || '');
    const [copied, setCopied] = useState(false);

    const handleCopy = () => {
        navigator.clipboard.writeText(String(children).replace(/\n$/, ''));
        setCopied(true);
        setTimeout(() => setCopied(false), 2000);
    };

    return !inline && match ? (
        <div className="relative group mt-5 mb-5 shadow-lg rounded-lg overflow-hidden border border-white/5">
            <div className="flex justify-between items-center bg-[#18181b] px-4 py-2 border-b border-white/5">
                <span className="text-xs font-mono-custom text-slate-400 capitalize">{match[1]}</span>
                <button
                    onClick={handleCopy}
                    className="flex items-center gap-1.5 text-xs text-slate-400 hover:text-slate-200 transition-colors"
                >
                    {copied ? <Check size={14} className="text-emerald-400" /> : <Copy size={14} />}
                    <span>{copied ? 'Copied!' : 'Copy code'}</span>
                </button>
            </div>
            <SyntaxHighlighter
                {...props}
                style={vscDarkPlus}
                language={match[1]}
                PreTag="div"
                className="!m-0 !bg-[#0b0b0f] !p-4 font-mono-custom text-sm custom-scrollbar"
            >
                {String(children).replace(/\n$/, '')}
            </SyntaxHighlighter>
        </div>
    ) : (
        <code {...props} className={`${className} bg-primary/10 px-1.5 py-0.5 rounded-md text-[13px] font-mono-custom text-primary`}>
            {children}
        </code>
    );
};

export const ChatMessage: React.FC<{ message: MessageProps }> = ({ message }) => {
    const isUser = message.role === 'user';
    const [displayedContent, setDisplayedContent] = useState(
        !isUser && message.isNew ? '' : message.content
    );

    useEffect(() => {
        if (isUser || !message.isNew) {
            setDisplayedContent(message.content);
            return;
        }

        let index = 0;
        setDisplayedContent('');
        const timer = setInterval(() => {
            if (index < message.content.length) {
                setDisplayedContent(message.content.substring(0, index + 1));
                index++;
            } else {
                clearInterval(timer);
            }
        }, 15);

        return () => clearInterval(timer);
    }, [message.content, isUser, message.isNew]);

    return (
        <motion.div
            initial={{ opacity: 0, y: 15 }}
            animate={{ opacity: 1, y: 0 }}
            exit={{ opacity: 0, scale: 0.95 }}
            transition={{ duration: 0.3, ease: 'easeOut' }}
            className={`flex w-full mb-6 ${isUser ? 'justify-end' : 'justify-start'}`}
        >
            <div className={`flex max-w-[85%] sm:max-w-[75%] gap-4 ${isUser ? 'flex-row-reverse' : 'flex-row items-start'}`}>
                {/* Avatar */}
                <div className={`flex-shrink-0 w-8 h-8 rounded-full flex items-center justify-center mt-1 
                    ${isUser ? 'bg-[#3c4043]' : 'bg-transparent'}
                `}>
                    {isUser ? (
                        <User size={18} className="text-white" />
                    ) : (
                        <div className="w-8 h-8 relative flex items-center justify-center bg-white/5 rounded-full border border-white/10 shadow-[0_0_15px_rgba(139,92,246,0.2)]">
                            <svg width="18" height="18" viewBox="0 0 24 24" fill="none" className="text-primary drop-shadow-[0_0_8px_rgba(139,92,246,0.6)]">
                                <path d="M12 2L14.809 9.19098L22 12L14.809 14.809L12 22L9.19098 14.809L2 12L9.19098 9.19098L12 2Z" fill="currentColor" />
                            </svg>
                        </div>
                    )}
                </div>

                {/* Message Bubble */}
                <div className={`
                    leading-[1.65] text-[16px] break-words overflow-x-auto
                    ${isUser
                        ? 'bg-primary text-white px-5 py-3 rounded-[24px] rounded-br-sm shadow-[0_4px_15px_rgba(139,92,246,0.15)]'
                        : 'text-foreground pt-1.5'
                    }
                `}>
                    {isUser ? (
                        message.content
                    ) : (
                        <div className="markdown-prose z-10 w-full">
                            <ReactMarkdown
                                remarkPlugins={[remarkGfm]}
                                components={{
                                    code: CodeBlock,
                                    p: ({ children }) => <p className="mb-4 last:mb-0 leading-[1.65] text-slate-300">{children}</p>,
                                    ul: ({ children }) => <ul className="list-disc ml-5 mb-4 space-y-1 text-slate-300">{children}</ul>,
                                    ol: ({ children }) => <ol className="list-decimal ml-5 mb-4 space-y-1 text-slate-300">{children}</ol>,
                                    li: ({ children }) => <li className="mb-1 leading-[1.65]">{children}</li>,
                                    a: ({ href, children }) => <a href={href} className="text-primary hover:text-primary-hover underline underline-offset-2 transition-colors" target="_blank" rel="noreferrer">{children}</a>,
                                    h1: ({ children }) => <h1 className="text-2xl font-bold mb-4 mt-6 text-slate-100">{children}</h1>,
                                    h2: ({ children }) => <h2 className="text-xl font-bold mb-3 mt-5 text-slate-100">{children}</h2>,
                                    h3: ({ children }) => <h3 className="text-lg font-bold mb-3 mt-4 text-slate-100">{children}</h3>,
                                    strong: ({ children }) => <strong className="font-semibold text-slate-200">{children}</strong>,
                                    blockquote: ({ children }) => <blockquote className="border-l-4 border-primary/50 pl-4 py-1.5 my-4 bg-white/5 rounded-r-lg text-slate-300 italic">{children}</blockquote>
                                }}
                            >
                                {displayedContent}
                            </ReactMarkdown>
                            {!displayedContent && !isUser && message.isNew && (
                                <div className="flex gap-1.5 mt-2 h-6 items-center">
                                    <span className="w-1.5 h-1.5 bg-primary/80 rounded-full animate-bounce" style={{ animationDelay: '0ms' }} />
                                    <span className="w-1.5 h-1.5 bg-primary/80 rounded-full animate-bounce" style={{ animationDelay: '150ms' }} />
                                    <span className="w-1.5 h-1.5 bg-primary/80 rounded-full animate-bounce" style={{ animationDelay: '300ms' }} />
                                </div>
                            )}
                        </div>
                    )}
                </div>
            </div>
        </motion.div>
    );
};