Spaces:
Sleeping
Sleeping
// Inspired by Chatbot-UI and modified to fit the needs of this project | |
// @see https://github.com/mckaywrigley/chatbot-ui/blob/main/components/Chat/ChatMessage.tsx | |
import remarkGfm from 'remark-gfm'; | |
import remarkMath from 'remark-math'; | |
import rehypeRaw from 'rehype-raw'; | |
import { useMemo, useState } from 'react'; | |
import { cn } from '@/lib/utils'; | |
import { CodeBlock } from '@/components/ui/CodeBlock'; | |
import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown'; | |
import { IconOpenAI, IconUser } from '@/components/ui/Icons'; | |
import { MessageBase } from '../../lib/types'; | |
import { | |
Tooltip, | |
TooltipContent, | |
TooltipTrigger, | |
} from '@/components/ui/Tooltip'; | |
import Img from '../ui/Img'; | |
import { getCleanedUpMessages } from '@/lib/messageUtils'; | |
import Loading from '../ui/Loading'; | |
import { | |
Table, | |
TableCell, | |
TableHead, | |
TableHeader, | |
TableRow, | |
} from '../ui/Table'; | |
import { Button } from '../ui/Button'; | |
import { Dialog, DialogContent } from '../ui/Dialog'; | |
export interface ChatMessageProps { | |
message: MessageBase; | |
isLoading: boolean; | |
} | |
const Markdown: React.FC<{ | |
content: string; | |
setDetails?: (val: string) => void; | |
}> = ({ content, setDetails }) => { | |
return ( | |
<> | |
<MemoizedReactMarkdown | |
className="break-words overflow-auto" | |
remarkPlugins={[remarkGfm, remarkMath]} | |
rehypePlugins={[rehypeRaw] as any} | |
components={{ | |
table({ children, ...props }) { | |
return <Table {...props}>{children}</Table>; | |
}, | |
thead({ children, ...props }) { | |
return <TableHeader {...props}>{children}</TableHeader>; | |
}, | |
th({ children, ...props }) { | |
return <TableHead {...props}>{children}</TableHead>; | |
}, | |
tr({ children, ...props }) { | |
return <TableRow {...props}>{children}</TableRow>; | |
}, | |
td({ children, ...props }) { | |
return <TableCell {...props}>{children}</TableCell>; | |
}, | |
button({ children, ...props }) { | |
if ('data-details' in props && setDetails) { | |
return ( | |
<Button | |
{...props} | |
onClick={() => | |
setDetails(decodeURI(props['data-details'] as string)) | |
} | |
> | |
{children} | |
</Button> | |
); | |
} | |
return <Button {...props}>{children}</Button>; | |
}, | |
p({ children, ...props }) { | |
if ( | |
props.node.children.some( | |
child => child.type === 'element' && child.tagName === 'img', | |
) | |
) { | |
return ( | |
<p className="flex flex-wrap gap-2 items-start">{children}</p> | |
); | |
} | |
return ( | |
<p className="my-2 last:mb-0 whitespace-pre-line">{children}</p> | |
); | |
}, | |
img(props) { | |
if (props.src?.endsWith('.mp4')) { | |
return ( | |
<video src={props.src} controls width={500} height={500} /> | |
); | |
} | |
return ( | |
<Tooltip> | |
<TooltipTrigger asChild> | |
<Img | |
src={props.src ?? '/landing.png'} | |
alt={props.alt ?? 'answer-image'} | |
quality={100} | |
className="cursor-zoom-in" | |
sizes="(min-width: 66em) 25vw, | |
(min-width: 44em) 40vw, | |
100vw" | |
/> | |
</TooltipTrigger> | |
<TooltipContent> | |
<Img | |
className="m-2" | |
src={props.src ?? '/landing.png'} | |
alt={props.alt ?? 'answer-image'} | |
quality={100} | |
width={500} | |
/> | |
</TooltipContent> | |
</Tooltip> | |
); | |
}, | |
code({ node, inline, className, children, ...props }) { | |
// if (children.length) { | |
// if (children[0] == 'β') { | |
// return ( | |
// <span className="mt-1 cursor-default animate-pulse">β</span> | |
// ); | |
// } | |
// children[0] = (children[0] as string).replace('`β`', 'β'); | |
// } | |
const match = /language-(\w+)/.exec(className || ''); | |
// if (inline) { | |
// return ( | |
// <code className={className} {...props}> | |
// {children} | |
// </code> | |
// ); | |
// } | |
return ( | |
<CodeBlock | |
key={Math.random()} | |
language={(match && match[1]) || ''} | |
value={String(children).replace(/\n$/, '')} | |
{...props} | |
/> | |
); | |
}, | |
}} | |
> | |
{content} | |
</MemoizedReactMarkdown> | |
</> | |
); | |
}; | |
export function ChatMessage({ | |
message, | |
isLoading, | |
...props | |
}: ChatMessageProps) { | |
const { logs, content } = useMemo(() => { | |
return getCleanedUpMessages({ | |
content: message.content, | |
role: message.role, | |
}); | |
}, [message.content, message.role]); | |
const [details, setDetails] = useState<string>(''); | |
return ( | |
<div className={cn('group relative mb-4 flex items-start')} {...props}> | |
<div | |
className={cn( | |
'flex size-8 shrink-0 select-none items-center justify-center rounded-md border shadow', | |
message.role === 'user' | |
? 'bg-background' | |
: 'bg-primary text-primary-foreground', | |
)} | |
> | |
{message.role === 'user' ? <IconUser /> : <IconOpenAI />} | |
</div> | |
<div className="flex-1 px-1 ml-4 space-y-2 overflow-hidden"> | |
{logs && <Markdown content={logs} setDetails={setDetails} />} | |
{/* <ChatMessageActions message={message} /> */} | |
{isLoading && <Loading />} | |
</div> | |
<Dialog open={!!details} onOpenChange={open => !open && setDetails('')}> | |
<DialogContent className="w-11/12"> | |
<Markdown content={details} /> | |
</DialogContent> | |
</Dialog> | |
</div> | |
); | |
} | |