vision-agent / components /chat /ChatMessage.tsx
wuyiqunLu
feat: format the responses (#15)
f0f12a9 unverified
raw
history blame
5.11 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 { cn } from '@/lib/utils';
import { CodeBlock } from '@/components/ui/CodeBlock';
import { MemoizedReactMarkdown } from '@/components/chat/MemoizedReactMarkdown';
import { IconOpenAI, IconUser } from '@/components/ui/Icons';
import { ChatMessageActions } from '@/components/chat/ChatMessageActions';
import { MessageBase } from '../../lib/types';
export interface ChatMessageProps {
message: MessageBase;
}
const PAIRS: Record<string, string> = {
'┍': 'β”‘',
'┝': 'β”₯',
'β”œ': '─',
'β”•': 'β”™',
};
const MIDDLE_STARTER = '┝';
const MIDDLE_SEPARATOR = 'β”Ώ';
export function ChatMessage({ message, ...props }: ChatMessageProps) {
const cleanupMessage = ({ content, role }: MessageBase) => {
if (role === 'user') {
return {
content,
};
}
const [logs = '', answer = ''] = content.split('<ANSWER>');
const cleanedLogs = [];
let left = 0;
let right = 0;
while (right < logs.length) {
if (Object.keys(PAIRS).includes(content[right])) {
cleanedLogs.push(content.substring(left, right));
left = right++;
while (
right < content.length &&
PAIRS[content[left]] !== content[right]
) {
right++;
}
if (content[left] === MIDDLE_STARTER) {
// add the text alignment so it can be shown as a table
const separators = logs
.substring(left, right)
.split(MIDDLE_SEPARATOR).length;
if (separators > 0) {
cleanedLogs.push(
Array(separators + 1)
.fill('|')
.join(' :- '),
);
}
}
left = ++right;
} else {
right++;
}
}
cleanedLogs.push(content.substring(left, right));
return {
logs: cleanedLogs
.join('')
.replace(/β”‚/g, '|')
.split('|\n\n|')
.join('|\n|'),
content: answer.replace('</</ANSWER>', '').replace('</ANSWER>', ''),
};
};
const { logs, content } = cleanupMessage(message);
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 && message.role !== 'user' && (
<div className="bg-slate-100 mb-4 p-4 max-h-60 overflow-auto">
<div className="text-xl font-bold">Thinking Process</div>
<MemoizedReactMarkdown
className="break-words text-sm"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return (
<p className="my-2 last:mb-0 whitespace-pre-line">
{children}
</p>
);
},
code({ children, ...props }) {
return (
<code className="whitespace-pre-line">{children}</code>
);
},
}}
>
{logs}
</MemoizedReactMarkdown>
</div>
)}
<MemoizedReactMarkdown
className="break-words"
remarkPlugins={[remarkGfm, remarkMath]}
components={{
p({ children }) {
return (
<p className="my-2 last:mb-0 whitespace-pre-line">{children}</p>
);
},
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>
<ChatMessageActions message={message} />
</div>
</div>
);
}