vision-agent / components /chat /ChatMessage.tsx
wuyiqunLu
feat: parse new msg format (#57)
63d0776 unverified
raw
history blame
6.23 kB
// 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>
);
}