import { ThreadMessage } from '@janhq/core' import { RenderMarkdown } from './RenderMarkdown' import { Fragment, memo, useCallback, useMemo, useState } from 'react' import { IconCopy, IconCopyCheck, IconRefresh, IconTrash, IconPencil, IconInfoCircle, } from '@tabler/icons-react' import { useAppState } from '@/hooks/useAppState' import { cn } from '@/lib/utils' import { useMessages } from '@/hooks/useMessages' import ThinkingBlock from '@/containers/ThinkingBlock' import ToolCallBlock from '@/containers/ToolCallBlock' import { useChat } from '@/hooks/useChat' import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader, DialogTitle, DialogTrigger, } from '@/components/ui/dialog' import { Button } from '@/components/ui/button' import { Textarea } from '@/components/ui/textarea' import { toast } from 'sonner' import { Tooltip, TooltipContent, TooltipTrigger, } from '@/components/ui/tooltip' import { formatDate } from '@/utils/formatDate' import { AvatarEmoji } from '@/containers/AvatarEmoji' import CodeEditor from '@uiw/react-textarea-code-editor' import '@uiw/react-textarea-code-editor/dist.css' const CopyButton = ({ text }: { text: string }) => { const [copied, setCopied] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( ) } // Use memo to prevent unnecessary re-renders, but allow re-renders when props change export const ThreadContent = memo( ( item: ThreadMessage & { isLastMessage?: boolean index?: number showAssistant?: boolean } ) => { const [message, setMessage] = useState(item.content?.[0]?.text?.value || '') // Use useMemo to stabilize the components prop const linkComponents = useMemo( () => ({ a: ({ ...props }) => ( ), }), [] ) const image = useMemo(() => item.content?.[0]?.image_url, [item]) const { streamingContent } = useAppState() const text = useMemo( () => item.content.find((e) => e.type === 'text')?.text?.value ?? '', [item.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 { getMessages, deleteMessage } = useMessages() const { sendMessage } = useChat() const regenerate = useCallback(() => { // Only regenerate assistant message is allowed deleteMessage(item.thread_id, item.id) const threadMessages = getMessages(item.thread_id) let toSendMessage = threadMessages.pop() while (toSendMessage && toSendMessage?.role !== 'user') { deleteMessage(toSendMessage.thread_id, toSendMessage.id ?? '') toSendMessage = threadMessages.pop() } if (toSendMessage) sendMessage(toSendMessage.content?.[0]?.text?.value || '') }, [deleteMessage, getMessages, item, sendMessage]) const editMessage = useCallback( (messageId: string) => { const threadMessages = getMessages(item.thread_id) const index = threadMessages.findIndex((msg) => msg.id === messageId) if (index === -1) return // Delete all messages after the edited message for (let i = threadMessages.length - 1; i >= index; i--) { deleteMessage(threadMessages[i].thread_id, threadMessages[i].id) } sendMessage(message) }, [deleteMessage, getMessages, item.thread_id, message, sendMessage] ) const isToolCalls = item.metadata && 'tool_calls' in item.metadata && Array.isArray(item.metadata.tool_calls) && item.metadata.tool_calls.length const assistant = item.metadata?.assistant as | { avatar?: React.ReactNode; name?: React.ReactNode } | undefined return ( {item.content?.[0]?.text && item.role === 'user' && (

{item.content?.[0].text.value}

Edit

Edit Message