import { IconBrain, IconCheck, IconChevronDown, IconCopy, IconDownload, IconFileText, IconTool, } from "@tabler/icons-react" import { useState } from "react" import { useTranslation } from "react-i18next" import ReactMarkdown from "react-markdown" import rehypeHighlight from "rehype-highlight" import rehypeRaw from "rehype-raw" import rehypeSanitize from "rehype-sanitize" import remarkGfm from "remark-gfm" import { MessageCodeBlock, MarkdownCodeBlock, } from "@/components/chat/message-code-block" import { Button } from "@/components/ui/button" import { formatMessageTime } from "@/hooks/use-pico-chat" import { useCopyToClipboard } from "@/hooks/use-copy-to-clipboard" import { cn } from "@/lib/utils" import { type AssistantMessageKind, type ChatAttachment, type ChatToolCall, } from "@/store/chat" interface AssistantMessageProps { content: string attachments?: ChatAttachment[] kind?: AssistantMessageKind modelName?: string toolCalls?: ChatToolCall[] timestamp?: string | number } export function AssistantMessage({ content, attachments = [], kind = "normal", modelName, toolCalls = [], timestamp = "", }: AssistantMessageProps) { const { t } = useTranslation() const { copy, isCopied } = useCopyToClipboard() const isThought = kind === "thought" const isToolCalls = kind === "tool_calls" const isCollapsedBlock = isThought || isToolCalls const hasText = content.trim().length > 0 const hasToolCalls = toolCalls.length > 0 const imageAttachments = attachments.filter( (attachment) => attachment.type === "image", ) const fileAttachments = attachments.filter( (attachment) => attachment.type !== "image", ) const [isExpanded, setIsExpanded] = useState(true) const formattedTimestamp = timestamp !== "" ? formatMessageTime(timestamp) : "" const collapsedLabel = isThought ? t("chat.reasoningLabel") : t("chat.toolCallsLabel") const copyMessageLabel = isCopied ? t("chat.copiedLabel") : t("chat.copyMessage") const trimmedModelName = modelName?.trim() ?? "" return (
{!isCollapsedBlock && (
PicoClaw {trimmedModelName && ( <> {trimmedModelName} )} {formattedTimestamp && ( <> {formattedTimestamp} )}
)} {(hasText || isCollapsedBlock || hasToolCalls) && (
{isCollapsedBlock && (
setIsExpanded(!isExpanded)} >
{isThought ? ( ) : ( )} {collapsedLabel} {trimmedModelName && ( {trimmedModelName} )}
{formattedTimestamp && ( {formattedTimestamp} )}
)} {(!isCollapsedBlock || isExpanded) && isToolCalls && hasToolCalls && (
{toolCalls.map((toolCall, index) => { const explanation = toolCall.extraContent?.toolFeedbackExplanation?.trim() ?? "" const toolName = toolCall.function?.name?.trim() ?? "" const toolArguments = toolCall.function?.arguments?.trim() ?? "" const hasFunctionSummary = toolName || toolArguments if (!explanation && !hasFunctionSummary) { return null } return (
0 && "border-border/20 border-t pt-3", )} > {explanation && (
{t("chat.toolCallExplanationLabel")}
{explanation}
)} {hasFunctionSummary && (
{t("chat.toolCallFunctionLabel")}
{toolName && !toolArguments && (
{toolName}
)} {toolArguments && ( )}
)}
) })}
)} {(!isCollapsedBlock || isExpanded) && !isToolCalls && hasText && (
{content}
)} {!isCollapsedBlock && hasText && ( )}
)} {imageAttachments.length > 0 && (
{imageAttachments.map((attachment, index) => ( {attachment.filename
))}
)} {fileAttachments.length > 0 && (
{fileAttachments.map((attachment, index) => (
{attachment.filename || "Download file"} {attachment.filename?.split(".").pop()?.toUpperCase() || "FILE"}
))}
)}
) }