import { useMemo, useState } from 'react'; import { useAppContext } from '../utils/app.context'; import { Message, PendingMessage } from '../utils/types'; import { classNames } from '../utils/misc'; import MarkdownDisplay, { CopyButton } from './MarkdownDisplay'; interface SplitMessage { content: PendingMessage['content']; thought?: string; isThinking?: boolean; } export default function ChatMessage({ msg, id, scrollToBottom, isPending, }: { msg: Message | PendingMessage; id?: string; scrollToBottom: (requiresNearBottom: boolean) => void; isPending?: boolean; }) { const { viewingConversation, replaceMessageAndGenerate, config } = useAppContext(); const [editingContent, setEditingContent] = useState(null); const timings = useMemo( () => msg.timings ? { ...msg.timings, prompt_per_second: (msg.timings.prompt_n / msg.timings.prompt_ms) * 1000, predicted_per_second: (msg.timings.predicted_n / msg.timings.predicted_ms) * 1000, } : null, [msg.timings] ); // for reasoning model, we split the message into content and thought // TODO: implement this as remark/rehype plugin in the future const { content, thought, isThinking }: SplitMessage = useMemo(() => { if (msg.content === null || msg.role !== 'assistant') { return { content: msg.content }; } let actualContent = ''; let thought = ''; let isThinking = false; let thinkSplit = msg.content.split('', 2); actualContent += thinkSplit[0]; while (thinkSplit[1] !== undefined) { // tag found thinkSplit = thinkSplit[1].split('', 2); thought += thinkSplit[0]; isThinking = true; if (thinkSplit[1] !== undefined) { // closing tag found isThinking = false; thinkSplit = thinkSplit[1].split('', 2); actualContent += thinkSplit[0]; } } return { content: actualContent, thought, isThinking }; }, [msg]); if (!viewingConversation) return null; const regenerate = async () => { replaceMessageAndGenerate(viewingConversation.id, msg.id, undefined, () => scrollToBottom(true) ); }; return (
{/* textarea for editing message */} {editingContent !== null && ( <>
)} {/* not editing content, render message */} {editingContent === null && ( <> {content === null ? ( <> {/* show loading dots for pending message */} ) : ( <> {/* render message as markdown */}
{thought && (
{isPending && isThinking ? ( Thinking ) : ( Thought Process )}
)}
)} {/* render timings if enabled */} {timings && config.showTokensPerSecond && (
Speed: {timings.predicted_per_second.toFixed(1)} t/s
Prompt
- Tokens: {timings.prompt_n}
- Time: {timings.prompt_ms} ms
- Speed: {timings.prompt_per_second.toFixed(1)} t/s
Generation
- Tokens: {timings.predicted_n}
- Time: {timings.predicted_ms} ms
- Speed: {timings.predicted_per_second.toFixed(1)} t/s
)} )}
{/* actions for each message */} {msg.content !== null && (
{/* user message */} {msg.role === 'user' && ( )} {/* assistant message */} {msg.role === 'assistant' && ( <> {!isPending && ( )} )}
)}
); }