import { ThreadMessage } from '@janhq/core' import { RenderMarkdown } from './RenderMarkdown' import { Fragment, memo, useCallback, useMemo, useState } from 'react' import { IconCopy, IconCopyCheck, IconRefresh, IconTrash, IconPencil, } 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' const CopyButton = ({ text }: { text: string }) => { const [copied, setCopied] = useState(false) const handleCopy = () => { navigator.clipboard.writeText(text) setCopied(true) setTimeout(() => setCopied(false), 2000) } return ( {copied ? ( <> Copied! > ) : ( Copy )} ) } // Use memo to prevent unnecessary re-renders, but allow re-renders when props change export const ThreadContent = memo( (item: ThreadMessage & { isLastMessage?: boolean; index?: number }) => { 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) const lastMessage = threadMessages[threadMessages.length - 1] if (!lastMessage) return deleteMessage(lastMessage.thread_id, lastMessage.id) sendMessage(lastMessage.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 return ( {item.content?.[0]?.text && item.role === 'user' && ( {item.content?.[0].text.value} Edit Edit Message { setMessage(e.target.value) }} className="mt-2 resize-none" onKeyDown={(e) => { // Prevent key from being captured by parent components e.stopPropagation() }} /> Cancel { editMessage(item.id) toast.success('Edit Message', { id: 'edit-message', description: 'Message edited successfully. Please wait for the model to respond.', }) }} > Save { deleteMessage(item.thread_id, item.id) }} > Delete )} {item.content?.[0]?.text && item.role !== 'user' && ( <> {reasoningSegment && ( )} {isToolCalls && item.metadata?.tool_calls ? ( <> {(item.metadata.tool_calls as ToolCall[]).map((toolCall) => ( ))} > ) : null} {!isToolCalls && ( { deleteMessage(item.thread_id, item.id) }} > Delete {item.isLastMessage && ( Regenerate )} )} > )} {item.type === 'image_url' && image && ( {image.detail && {image.detail}} )} ) } )
Copy
{item.content?.[0].text.value}
Edit
Delete
Regenerate
{image.detail}