// 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 {
IconCheckCircle,
IconChevronDoubleRight,
IconCodeWrap,
IconCrossCircle,
IconLandingAI,
IconListUnordered,
IconTerminalWindow,
IconUser,
IconOutput,
IconLog,
} from '@/components/ui/Icons';
import { MessageBase } from '../../lib/types';
import Img from '../ui/Img';
import { ChunkBody, CodeResult, formatStreamLogs } from '@/lib/messageUtils';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '../ui/Table';
import { Button } from '../ui/Button';
import { Separator } from '../ui/Separator';
import {
Tooltip,
TooltipContent,
TooltipTrigger,
} from '@/components/ui/Tooltip';
import { Dialog, DialogContent, DialogTrigger } from '../ui/Dialog';
export interface ChatMessageProps {
message: MessageBase;
isLoading: boolean;
}
const Markdown: React.FC<{
content: string;
setDetails?: (val: string) => void;
}> = ({ content, setDetails }) => {
return (
<>
{children};
},
thead({ children, ...props }) {
return {children};
},
th({ children, ...props }) {
return {children};
},
tr({ children, ...props }) {
return {children};
},
td({ children, ...props }) {
return {children};
},
button({ children, ...props }) {
if ('data-details' in props && setDetails) {
return (
);
}
return ;
},
p({ children, ...props }) {
if (
props.node.children.some(
child => child.type === 'element' && child.tagName === 'img',
)
) {
return (
{children}
);
}
return {children}
;
},
img(props) {
if (props.src?.endsWith('.mp4')) {
return (
);
}
return (
);
},
code({ node, inline, className, children, ...props }) {
const match = /language-(\w+)/.exec(className || '');
return (
);
},
}}
>
{content}
>
);
};
export function ChatMessage({ message, isLoading }: ChatMessageProps) {
const { role, content } = message;
return role === 'user' ? (
) : (
);
}
const UserChatMessage: React.FC<{
content: string;
}> = ({ content }) => {
return (
);
};
const ChunkStatusToIconDict: Record = {
started: ,
completed: ,
running: ,
failed: ,
};
const ChunkTypeToTextDict: Record = {
plans: 'Creating instructions',
tools: 'Retrieving tools',
code: 'Generating code',
final_code: 'Final result',
};
const ChunkPayloadAction: React.FC<{
payload: ChunkBody['payload'];
}> = ({ payload }) => {
if (Array.isArray(payload)) {
// [{title: 123, content, 345}, {title: ..., content: ...}] => ['title', 'content']
const keyArray = Array.from(
payload.reduce((acc, curr) => {
Object.keys(curr).forEach(key => acc.add(key));
return acc;
}, new Set()),
);
return (
);
} else {
return (
);
}
};
const CodeResultDisplay: React.FC<{
codeResult: CodeResult;
}> = ({ codeResult }) => {
const { code, test, result } = codeResult;
const getDetail = () => {
if (!result) return {};
try {
const detail = JSON.parse(result);
return {
results: detail.results,
stderr: detail.logs.stderr,
stdout: detail.logs.stdout,
};
} catch {
return {};
}
};
const { results, stderr, stdout } = getDetail();
return (
{Array.isArray(stdout) && (
)}
{Array.isArray(stderr) && (
)}
);
};
const AssistantChatMessage: React.FC<{
content: string;
}> = ({ content }) => {
const [formattedSections, codeResult] = useMemo(
() => formatStreamLogs(content),
[content],
);
return (
{formattedSections.map(section => (
{ChunkStatusToIconDict[section.status]}
{ChunkTypeToTextDict[section.type]}
))}
{codeResult &&
}
);
};
export default UserChatMessage;