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'; import { ChevronLeftIcon, ChevronRightIcon } from '@heroicons/react/24/outline'; interface SplitMessage { content: PendingMessage['content']; thought?: string; isThinking?: boolean; } export default function ChatMessage({ msg, siblingLeafNodeIds, siblingCurrIdx, id, onRegenerateMessage, onEditMessage, onChangeSibling, isPending, }: { msg: Message | PendingMessage; siblingLeafNodeIds: Message['id'][]; siblingCurrIdx: number; id?: string; onRegenerateMessage(msg: Message): void; onEditMessage(msg: Message, content: string): void; onChangeSibling(sibling: Message['id']): void; isPending?: boolean; }) { const { viewingChat, 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] ); const nextSibling = siblingLeafNodeIds[siblingCurrIdx + 1]; const prevSibling = siblingLeafNodeIds[siblingCurrIdx - 1]; // 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 (!viewingChat) return null; 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 )}
)} {msg.extra && msg.extra.length > 0 && (
Extra content
{msg.extra.map( (extra, i) => extra.type === 'textFile' ? (
{extra.name}
{extra.content}
) : extra.type === 'context' ? (
{extra.content}
) : null // TODO: support other extra types )}
)}
)} {/* 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 && (
{siblingLeafNodeIds && siblingLeafNodeIds.length > 1 && (
{siblingCurrIdx + 1} / {siblingLeafNodeIds.length}
)} {/* user message */} {msg.role === 'user' && ( )} {/* assistant message */} {msg.role === 'assistant' && ( <> {!isPending && ( )} )}
)}
); }