import React, { Fragment, useEffect, useMemo, useRef, useState } from 'react' import { Message, TextContentBlock } 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 UserAvatar from '@/components/UserAvatar' import LogoMark from '@/containers/Brand/Logo/Mark' import useAssistantQuery from '@/hooks/useAssistantQuery' import { useClipboard } from '@/hooks/useClipboard' import { usePath } from '@/hooks/usePath' import { displayDate } from '@/utils/datetime' import { openFileTitle } from '@/utils/titleUtils' import EditChatInput from '../EditChatInput' import MessageToolbar from '../MessageToolbar' import { editMessageAtom } from '@/helpers/atoms/ChatMessage.atom' type Props = { isLatestMessage: boolean msg: Message } const SimpleTextMessage: React.FC = ({ isLatestMessage, msg }) => { const [text, setText] = useState('') const { data: assistants } = useAssistantQuery() const editMessage = useAtomValue(editMessageAtom) const clipboard = useClipboard({ timeout: 1000 }) const senderName = useMemo(() => { if (msg.role === 'user') return msg.role const assistantId = msg.assistant_id if (!assistantId) return msg.role const assistant = assistants?.find( (assistant) => assistant.id === assistantId ) return assistant?.name ?? msg.role }, [assistants, msg.assistant_id, msg.role]) useEffect(() => { if (msg.content && msg.content.length > 0) { const message = msg.content[0] if (message && message.type === 'text') { const textBlockContent = message as TextContentBlock setText(textBlockContent.text.value) } } }, [msg.content]) const marked = useMemo(() => { const markedParser = 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) => Renderer.prototype.link ?.apply(this, [href, title, text]) .replace('
              ${code}
            
` }, }, } ) markedParser.use(markedKatex({ throwOnError: false })) return markedParser }, [clipboard.copied]) const isUser = msg.role === 'user' const { onViewFileContainer } = usePath() const parsedText = useMemo(() => marked.parse(text), [marked, text]) const [tokenCount, setTokenCount] = useState(0) const [lastTimestamp, setLastTimestamp] = useState() const [tokenSpeed, setTokenSpeed] = useState(0) 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 (msg.status !== 'in_progress') { 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 (msg.content && msg.content.length > 0) { const message = msg.content[0] if (message && message.type === 'text') { const textContentBlock = message as TextContentBlock if (textContentBlock.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 }, [msg.content]) return (
{isUser ? : }
{senderName}

{displayDate(msg.created_at)}

{isLatestMessage && (msg.status === 'in_progress' || tokenSpeed > 0) && (

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

)}
<> {msg.content[0]?.type === 'image_file' && (
{/* */} {/* onViewFile(`${msg.content[0]?.text.annotations[0]}`) */} {/* } */} {/* /> */}
} content={{openFileTitle()}} />
)} {isUser ? ( {editMessage === msg.id ? ( ) : (
{text}
)}
) : (
)}
) } export default SimpleTextMessage