Files
DocsGPT/frontend/src/conversation/ConversationBubble.tsx
2023-10-16 18:59:31 -03:00

222 lines
8.1 KiB
TypeScript

import { forwardRef, useState } from 'react';
import Avatar from '../Avatar';
import { FEEDBACK, MESSAGE_TYPE } from './conversationModels';
import classes from './ConversationBubble.module.css';
import Alert from './../assets/alert.svg';
import { ReactComponent as Like } from './../assets/like.svg';
import { ReactComponent as Dislike } from './../assets/dislike.svg';
import { ReactComponent as Copy } from './../assets/copy.svg';
import { ReactComponent as Checkmark } from './../assets/checkmark.svg';
import ReactMarkdown from 'react-markdown';
import copy from 'copy-to-clipboard';
import { Prism as SyntaxHighlighter } from 'react-syntax-highlighter';
import { vscDarkPlus } from 'react-syntax-highlighter/dist/cjs/styles/prism';
const DisableSourceFE = import.meta.env.VITE_DISABLE_SOURCE_FE || false;
const ConversationBubble = forwardRef<
HTMLDivElement,
{
message: string;
type: MESSAGE_TYPE;
className?: string;
feedback?: FEEDBACK;
handleFeedback?: (feedback: FEEDBACK) => void;
sources?: { title: string; text: string }[];
}
>(function ConversationBubble(
{ message, type, className, feedback, handleFeedback, sources },
ref,
) {
const [showFeedback, setShowFeedback] = useState(false);
const [openSource, setOpenSource] = useState<number | null>(null);
const [copied, setCopied] = useState(false);
const handleCopyClick = (text: string) => {
copy(text);
setCopied(true);
// Reset copied to false after a few seconds
setTimeout(() => {
setCopied(false);
}, 2000);
};
let bubble;
if (type === 'QUESTION') {
bubble = (
<div ref={ref} className={`flex flex-row-reverse self-end ${className}`}>
<Avatar className="mt-2 text-2xl" avatar="🧑‍💻"></Avatar>
<div className="mr-2 ml-10 flex items-center rounded-3xl bg-purple-30 p-3.5 text-white">
<ReactMarkdown className="whitespace-pre-wrap break-all">
{message}
</ReactMarkdown>
</div>
</div>
);
} else {
bubble = (
<div
ref={ref}
className={`flex self-start ${className} flex-col`}
onMouseEnter={() => setShowFeedback(true)}
onMouseLeave={() => setShowFeedback(false)}
>
<div className="flex self-start">
<Avatar className="mt-2 text-2xl" avatar="🦖"></Avatar>
<div
className={`ml-2 mr-5 flex flex-col rounded-3xl bg-gray-1000 p-3.5 ${
type === 'ERROR'
? 'flex-row rounded-full border border-transparent bg-[#FFE7E7] p-2 py-5 text-sm font-normal text-red-3000 dark:border-red-2000 dark:text-white'
: 'flex-col rounded-3xl'
}`}
>
{type === 'ERROR' && (
<img src={Alert} alt="alert" className="mr-2 inline" />
)}
<ReactMarkdown
className="max-w-screen-md whitespace-pre-wrap break-words"
components={{
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return !inline && match ? (
<SyntaxHighlighter
PreTag="div"
language={match[1]}
{...props}
style={vscDarkPlus}
>
{String(children).replace(/\n$/, '')}
</SyntaxHighlighter>
) : (
<code className={className ? className : ''} {...props}>
{children}
</code>
);
},
ul({ children }) {
return (
<ul
className={`list-inside list-disc whitespace-normal pl-4 ${classes.list}`}
>
{children}
</ul>
);
},
ol({ children }) {
return (
<ol
className={`list-inside list-decimal whitespace-normal pl-4 ${classes.list}`}
>
{children}
</ol>
);
},
}}
>
{message}
</ReactMarkdown>
{DisableSourceFE || type === 'ERROR' ? null : (
<>
<span className="mt-3 h-px w-full bg-[#DEDEDE]"></span>
<div className="mt-3 flex w-full flex-row flex-wrap items-center justify-start gap-2">
<div className="py-1 text-base font-semibold">Sources:</div>
<div className="flex flex-row flex-wrap items-center justify-start gap-2">
{sources?.map((source, index) => (
<div
key={index}
className={`max-w-fit cursor-pointer rounded-[28px] py-1 px-4 ${
openSource === index
? 'bg-[#007DFF]'
: 'bg-[#D7EBFD] hover:bg-[#BFE1FF]'
}`}
onClick={() =>
setOpenSource(openSource === index ? null : index)
}
>
<p
className={`truncate text-center text-base font-medium ${
openSource === index
? 'text-white'
: 'text-[#007DFF]'
}`}
>
{index + 1}. {source.title.substring(0, 45)}
</p>
</div>
))}
</div>
</div>
</>
)}
</div>
<div
className={`relative mr-2 flex items-center justify-center ${
type !== 'ERROR' && showFeedback ? '' : 'md:invisible'
}`}
>
{copied ? (
<Checkmark className="absolute left-2 top-4" />
) : (
<Copy
className={`absolute left-2 top-4 cursor-pointer fill-gray-4000 hover:stroke-gray-4000`}
onClick={() => {
handleCopyClick(message);
}}
></Copy>
)}
</div>
<div
className={`relative mr-2 flex items-center justify-center ${
feedback === 'LIKE' || (type !== 'ERROR' && showFeedback)
? ''
: 'md:invisible'
}`}
>
<Like
className={`absolute left-6 top-4 cursor-pointer ${
feedback === 'LIKE'
? 'fill-purple-30 stroke-purple-30'
: 'fill-none stroke-gray-4000 hover:fill-gray-4000'
}`}
onClick={() => handleFeedback?.('LIKE')}
></Like>
</div>
<div
className={`relative mr-10 flex items-center justify-center ${
feedback === 'DISLIKE' || (type !== 'ERROR' && showFeedback)
? ''
: 'md:invisible'
}`}
>
<Dislike
className={`absolute left-10 top-4 cursor-pointer ${
feedback === 'DISLIKE'
? 'fill-red-2000 stroke-red-2000'
: 'fill-none stroke-gray-4000 hover:fill-gray-4000'
}`}
onClick={() => handleFeedback?.('DISLIKE')}
></Dislike>
</div>
</div>
{sources && openSource !== null && sources[openSource] && (
<div className="ml-10 mt-2 max-w-[800px] rounded-xl bg-blue-200 p-2">
<p className="m-1 w-3/4 truncate text-xs text-gray-500">
Source: {sources[openSource].title}
</p>
<div className="m-2 rounded-xl border-2 border-gray-200 bg-white p-2">
<p className="text-black">{sources[openSource].text}</p>
</div>
</div>
)}
</div>
);
}
return bubble;
});
export default ConversationBubble;