import React, { useEffect, useRef, useState } from 'react' import { ChatCompletionRole, ContentType, MessageStatus, ThreadMessage, } from '@janhq/core' import { Tooltip } from '@janhq/joi' import hljs from 'highlight.js' import { useAtomValue } from 'jotai' import { FolderOpenIcon } from 'lucide-react' import { Marked, Renderer } from 'marked' import { markedHighlight } from 'marked-highlight' import markedKatex from 'marked-katex-extension' import { twMerge } from 'tailwind-merge' import LogoMark from '@/containers/Brand/Logo/Mark' import { useClipboard } from '@/hooks/useClipboard' import { usePath } from '@/hooks/usePath' import { toGibibytes } from '@/utils/converter' import { displayDate } from '@/utils/datetime' import { openFileTitle } from '@/utils/titleUtils' import EditChatInput from '../EditChatInput' import Icon from '../FileUploadPreview/Icon' import MessageToolbar from '../MessageToolbar' import { RelativeImage } from './RelativeImage' import { editMessageAtom, getCurrentChatMessagesAtom, } from '@/helpers/atoms/ChatMessage.atom' import { activeThreadAtom } from '@/helpers/atoms/Thread.atom' const SimpleTextMessage: React.FC = (props) => { let text = '' const isUser = props.role === ChatCompletionRole.User const isSystem = props.role === ChatCompletionRole.System const editMessage = useAtomValue(editMessageAtom) const activeThread = useAtomValue(activeThreadAtom) if (props.content && props.content.length > 0) { text = props.content[0]?.text?.value ?? '' } const clipboard = useClipboard({ timeout: 1000 }) const marked: Marked = new Marked( markedHighlight({ langPrefix: 'hljs', highlight(code, lang) { if (lang === undefined || lang === '') { return hljs.highlightAuto(code).value } try { return hljs.highlight(code, { language: lang }).value } catch (err) { return hljs.highlight(code, { language: 'javascript' }).value } }, }), { renderer: { link: (href, title, text) => { return Renderer.prototype.link ?.apply(this, [href, title, text]) .replace('
              ${code}
            
` }, }, } ) marked.use(markedKatex({ throwOnError: false })) const { onViewFile, onViewFileContainer } = usePath() const parsedText = marked.parse(text) const [tokenCount, setTokenCount] = useState(0) const [lastTimestamp, setLastTimestamp] = useState() const [tokenSpeed, setTokenSpeed] = useState(0) const messages = useAtomValue(getCurrentChatMessagesAtom) const codeBlockCopyEvent = useRef((e: Event) => { const target: HTMLElement = e.target as HTMLElement if (typeof target.className !== 'string') return null const isCopyActionClassName = target?.className.includes('copy-action') if (isCopyActionClassName) { const content = target?.parentNode?.querySelector('code')?.innerText ?? '' clipboard.copy(content) } }) useEffect(() => { document.addEventListener('click', codeBlockCopyEvent.current) return () => { // eslint-disable-next-line react-hooks/exhaustive-deps document.removeEventListener('click', codeBlockCopyEvent.current) } }, []) useEffect(() => { if (props.status !== MessageStatus.Pending) { return } const currentTimestamp = new Date().getTime() // Get current time in milliseconds if (!lastTimestamp) { // If this is the first update, just set the lastTimestamp and return if (props.content[0]?.text?.value !== '') setLastTimestamp(currentTimestamp) return } const timeDiffInSeconds = (currentTimestamp - lastTimestamp) / 1000 // Time difference in seconds const totalTokenCount = tokenCount + 1 const averageTokenSpeed = totalTokenCount / timeDiffInSeconds // Calculate average token speed setTokenSpeed(averageTokenSpeed) setTokenCount(totalTokenCount) // eslint-disable-next-line react-hooks/exhaustive-deps }, [props.content]) return (
{!isUser && !isSystem && } {isUser && (
)}
{isUser ? props.role : activeThread?.assistants[0].assistant_name ?? props.role}

{displayDate(props.created)}

{messages[messages.length - 1]?.id === props.id && (props.status === MessageStatus.Pending || tokenSpeed > 0) && (

Token Speed: {Number(tokenSpeed).toFixed(2)}t/s

)}
<> {props.content[0]?.type === ContentType.Image && (
onViewFile(`${props.content[0]?.text.annotations[0]}`) } />
} content={{openFileTitle()}} />
)} {props.content[0]?.type === ContentType.Pdf && (
onViewFile(`${props.id}.${props.content[0]?.type}`) } />
} content={{openFileTitle()}} />
{props.content[0].text.name?.replaceAll(/[-._]/g, ' ')}

{toGibibytes(Number(props.content[0].text.size))}

)} {isUser ? ( <> {editMessage === props.id ? (
) : (
{text}
)} ) : (
)}
) } export default React.memo(SimpleTextMessage)