import { AssistantMessage, Message } from "@ag-ui/core"; import { useEffect, useRef, useState } from "react"; import { Copy, Check, ThumbsUp, ThumbsDown, Volume2, RefreshCw, } from "lucide-react"; import { useCopilotChatConfiguration, CopilotChatDefaultLabels, } from "../../providers/CopilotChatConfigurationProvider"; import { twMerge } from "tailwind-merge"; import { Button } from "../../components/ui/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip"; import { useKatexStyles } from "../../hooks/useKatexStyles"; import { WithSlots, renderSlot } from "../../lib/slots"; import { Streamdown } from "streamdown"; import { copyToClipboard } from "@copilotkit/shared"; import CopilotChatToolCallsView from "./CopilotChatToolCallsView"; export type CopilotChatAssistantMessageProps = WithSlots< { markdownRenderer: typeof CopilotChatAssistantMessage.MarkdownRenderer; toolbar: typeof CopilotChatAssistantMessage.Toolbar; copyButton: typeof CopilotChatAssistantMessage.CopyButton; thumbsUpButton: typeof CopilotChatAssistantMessage.ThumbsUpButton; thumbsDownButton: typeof CopilotChatAssistantMessage.ThumbsDownButton; readAloudButton: typeof CopilotChatAssistantMessage.ReadAloudButton; regenerateButton: typeof CopilotChatAssistantMessage.RegenerateButton; toolCallsView: typeof CopilotChatToolCallsView; }, { onThumbsUp?: (message: AssistantMessage) => void; onThumbsDown?: (message: AssistantMessage) => void; onReadAloud?: (message: AssistantMessage) => void; onRegenerate?: (message: AssistantMessage) => void; message: AssistantMessage; messages?: Message[]; isRunning?: boolean; additionalToolbarItems?: React.ReactNode; toolbarVisible?: boolean; } & React.HTMLAttributes >; export function CopilotChatAssistantMessage({ message, messages, isRunning, onThumbsUp, onThumbsDown, onReadAloud, onRegenerate, additionalToolbarItems, toolbarVisible = true, markdownRenderer, toolbar, copyButton, thumbsUpButton, thumbsDownButton, readAloudButton, regenerateButton, toolCallsView, children, className, ...props }: CopilotChatAssistantMessageProps) { useKatexStyles(); const boundMarkdownRenderer = renderSlot( markdownRenderer, CopilotChatAssistantMessage.MarkdownRenderer, { content: message.content || "", }, ); const boundCopyButton = renderSlot( copyButton, CopilotChatAssistantMessage.CopyButton, { onClick: async () => { if (message.content) { return await copyToClipboard(message.content); } return false; }, }, ); const boundThumbsUpButton = renderSlot( thumbsUpButton, CopilotChatAssistantMessage.ThumbsUpButton, { onClick: onThumbsUp ? () => onThumbsUp(message) : undefined, }, ); const boundThumbsDownButton = renderSlot( thumbsDownButton, CopilotChatAssistantMessage.ThumbsDownButton, { onClick: onThumbsDown ? () => onThumbsDown(message) : undefined, }, ); const boundReadAloudButton = renderSlot( readAloudButton, CopilotChatAssistantMessage.ReadAloudButton, { onClick: onReadAloud ? () => onReadAloud(message) : undefined, }, ); const boundRegenerateButton = renderSlot( regenerateButton, CopilotChatAssistantMessage.RegenerateButton, { onClick: onRegenerate ? () => onRegenerate(message) : undefined, }, ); const boundToolbar = renderSlot( toolbar, CopilotChatAssistantMessage.Toolbar, { children: (
{boundCopyButton} {(onThumbsUp || thumbsUpButton) && boundThumbsUpButton} {(onThumbsDown || thumbsDownButton) && boundThumbsDownButton} {(onReadAloud || readAloudButton) && boundReadAloudButton} {(onRegenerate || regenerateButton) && boundRegenerateButton} {additionalToolbarItems}
), }, ); const boundToolCallsView = renderSlot( toolCallsView, CopilotChatToolCallsView, { message, messages, }, ); // Don't show toolbar if message has no content (only tool calls) const hasContent = !!(message.content && message.content.trim().length > 0); const isLatestAssistantMessage = message.role === "assistant" && messages?.[messages.length - 1]?.id === message.id; const shouldShowToolbar = toolbarVisible && hasContent && !(isRunning && isLatestAssistantMessage); if (children) { return (
{children({ markdownRenderer: boundMarkdownRenderer, toolbar: boundToolbar, toolCallsView: boundToolCallsView, copyButton: boundCopyButton, thumbsUpButton: boundThumbsUpButton, thumbsDownButton: boundThumbsDownButton, readAloudButton: boundReadAloudButton, regenerateButton: boundRegenerateButton, message, messages, isRunning, onThumbsUp, onThumbsDown, onReadAloud, onRegenerate, additionalToolbarItems, toolbarVisible: shouldShowToolbar, })}
); } return (
{boundMarkdownRenderer}
{boundToolCallsView} {shouldShowToolbar && boundToolbar}
); } // eslint-disable-next-line @typescript-eslint/no-namespace export namespace CopilotChatAssistantMessage { export const MarkdownRenderer: React.FC< Omit, "children"> & { content: string; } > = ({ content, className, ...props }) => ( {content ?? ""} ); export const Toolbar: React.FC> = ({ className, ...props }) => (
); export const ToolbarButton: React.FC< React.ButtonHTMLAttributes & { title: string; children: React.ReactNode; } > = ({ title, children, ...props }) => { return (

{title}

); }; export const CopyButton: React.FC< React.ButtonHTMLAttributes > = ({ className, title, onClick, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; const [copied, setCopied] = useState(false); const timerRef = useRef | null>(null); useEffect(() => { return () => { if (timerRef.current !== null) { clearTimeout(timerRef.current); } }; }, []); const handleClick = async (event: React.MouseEvent) => { let success = false; if (onClick) { // onClick may return a boolean indicating copy success const result = await Promise.resolve(onClick(event)); success = result === true; } if (success) { setCopied(true); if (timerRef.current !== null) { clearTimeout(timerRef.current); } timerRef.current = setTimeout(() => { timerRef.current = null; setCopied(false); }, 2000); } }; return ( {copied ? ( ) : ( )} ); }; export const ThumbsUpButton: React.FC< React.ButtonHTMLAttributes > = ({ title, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; return ( ); }; export const ThumbsDownButton: React.FC< React.ButtonHTMLAttributes > = ({ title, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; return ( ); }; export const ReadAloudButton: React.FC< React.ButtonHTMLAttributes > = ({ title, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; return ( ); }; export const RegenerateButton: React.FC< React.ButtonHTMLAttributes > = ({ title, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; return ( ); }; } CopilotChatAssistantMessage.MarkdownRenderer.displayName = "CopilotChatAssistantMessage.MarkdownRenderer"; CopilotChatAssistantMessage.Toolbar.displayName = "CopilotChatAssistantMessage.Toolbar"; CopilotChatAssistantMessage.CopyButton.displayName = "CopilotChatAssistantMessage.CopyButton"; CopilotChatAssistantMessage.ThumbsUpButton.displayName = "CopilotChatAssistantMessage.ThumbsUpButton"; CopilotChatAssistantMessage.ThumbsDownButton.displayName = "CopilotChatAssistantMessage.ThumbsDownButton"; CopilotChatAssistantMessage.ReadAloudButton.displayName = "CopilotChatAssistantMessage.ReadAloudButton"; CopilotChatAssistantMessage.RegenerateButton.displayName = "CopilotChatAssistantMessage.RegenerateButton"; export default CopilotChatAssistantMessage;