import React, { useMemo } from 'react' import { ChatCompletionRole, ContentType, ThreadMessage } from '@janhq/core' import { useAtomValue } from 'jotai' import 'katex/dist/katex.min.css' import { twMerge } from 'tailwind-merge' import LogoMark from '@/containers/Brand/Logo/Mark' import { displayDate } from '@/utils/datetime' import EditChatInput from '../EditChatInput' import MessageToolbar from '../MessageToolbar' import DocMessage from './DocMessage' import ImageMessage from './ImageMessage' import { MarkdownTextMessage } from './MarkdownTextMessage' import ThinkingBlock from './ThinkingBlock' import ToolCallBlock from './ToolCallBlock' import { activeAssistantAtom } from '@/helpers/atoms/Assistant.atom' import { editMessageAtom, tokenSpeedAtom, } from '@/helpers/atoms/ChatMessage.atom' import { selectedModelAtom } from '@/helpers/atoms/Model.atom' import { chatWidthAtom } from '@/helpers/atoms/Setting.atom' const MessageContainer: React.FC< ThreadMessage & { isCurrentMessage: boolean; index: number } > = (props) => { const isUser = props.role === ChatCompletionRole.User const isSystem = props.role === ChatCompletionRole.System const editMessage = useAtomValue(editMessageAtom) const activeAssistant = useAtomValue(activeAssistantAtom) const tokenSpeed = useAtomValue(tokenSpeedAtom) const chatWidth = useAtomValue(chatWidthAtom) const selectedModel = useAtomValue(selectedModelAtom) const text = useMemo( () => props.content.find((e) => e.type === ContentType.Text)?.text?.value ?? '', [props.content] ) const { reasoningSegment, textSegment } = useMemo(() => { const isThinking = text.includes('') && !text.includes('') if (isThinking) return { reasoningSegment: text, textSegment: '' } const match = text.match(/([\s\S]*?)<\/think>/) if (match?.index === undefined) return { reasoningSegment: undefined, textSegment: text } const splitIndex = match.index + match[0].length return { reasoningSegment: text.slice(0, splitIndex), textSegment: text.slice(splitIndex), } }, [text]) const image = useMemo( () => props.content.find((e) => e.type === ContentType.Image)?.image_url?.url, [props.content] ) const attachedFile = useMemo( () => 'attachments' in props && props.attachments?.length, [props] ) return (
{!(props.metadata && 'parent_id' in props.metadata) && (
{!isUser && !isSystem && }
{!isUser && ( <> {props.metadata && 'model' in props.metadata ? (props.metadata?.model as string) : props.isCurrentMessage ? selectedModel?.name : (activeAssistant?.assistant_name ?? props.role)} )}

{props.created_at && displayDate(props.created_at ?? Date.now() / 1000)}

)}
{((!!tokenSpeed && tokenSpeed.message === props.id && tokenSpeed.tokenSpeed > 0) || (props.metadata && 'token_speed' in props.metadata && !!props.metadata?.token_speed)) && (

Token Speed:{' '} {Number( props.metadata?.token_speed ?? tokenSpeed?.tokenSpeed ).toFixed(2)} t/s

)}
{editMessage !== props.id && }
<> {image && } {attachedFile && ( )} {editMessage === props.id ? (
) : (
{reasoningSegment && ( )}
)} {props.metadata && 'tool_calls' in props.metadata && Array.isArray(props.metadata.tool_calls) && props.metadata.tool_calls.length && ( <> {props.metadata.tool_calls.map((toolCall) => ( ))} )}
) } export default React.memo(MessageContainer)