import { useMemo, useState } from "react"; import { Copy, Check, Edit, ChevronLeft, ChevronRight } from "lucide-react"; import { useCopilotChatConfiguration, CopilotChatDefaultLabels, } from "../../providers/CopilotChatConfigurationProvider"; import { twMerge } from "tailwind-merge"; import { Button } from "../../components/ui/button"; import { UserMessage } from "@ag-ui/core"; import { Tooltip, TooltipContent, TooltipTrigger, } from "../../components/ui/tooltip"; import { renderSlot, WithSlots } from "../../lib/slots"; import { type ImageInputPart, type AudioInputPart, type VideoInputPart, type DocumentInputPart, copyToClipboard, } from "@copilotkit/shared"; import { CopilotChatAttachmentRenderer } from "./CopilotChatAttachmentRenderer"; function flattenUserMessageContent(content?: UserMessage["content"]): string { if (!content) { return ""; } if (typeof content === "string") { return content; } return content .map((part) => { if ( part && typeof part === "object" && "type" in part && (part as { type?: unknown }).type === "text" && typeof (part as { text?: unknown }).text === "string" ) { return (part as { text: string }).text; } return ""; }) .filter((text) => text.length > 0) .join("\n"); } type MediaPart = | ImageInputPart | AudioInputPart | VideoInputPart | DocumentInputPart; function getMediaParts(content: UserMessage["content"]): MediaPart[] { if (!content || typeof content === "string") return []; return content.filter( (part): part is MediaPart => part.type === "image" || part.type === "audio" || part.type === "video" || part.type === "document", ); } function getFilename(part: MediaPart): string | undefined { const meta = part.metadata; if ( meta != null && typeof meta === "object" && "filename" in meta && typeof meta.filename === "string" ) { return meta.filename; } return undefined; } export interface CopilotChatUserMessageOnEditMessageProps { message: UserMessage; } export interface CopilotChatUserMessageOnSwitchToBranchProps { message: UserMessage; branchIndex: number; numberOfBranches: number; } export type CopilotChatUserMessageProps = WithSlots< { messageRenderer: typeof CopilotChatUserMessage.MessageRenderer; toolbar: typeof CopilotChatUserMessage.Toolbar; copyButton: typeof CopilotChatUserMessage.CopyButton; editButton: typeof CopilotChatUserMessage.EditButton; branchNavigation: typeof CopilotChatUserMessage.BranchNavigation; }, { onEditMessage?: (props: CopilotChatUserMessageOnEditMessageProps) => void; onSwitchToBranch?: ( props: CopilotChatUserMessageOnSwitchToBranchProps, ) => void; message: UserMessage; branchIndex?: number; numberOfBranches?: number; additionalToolbarItems?: React.ReactNode; } & React.HTMLAttributes >; export function CopilotChatUserMessage({ message, onEditMessage, branchIndex, numberOfBranches, onSwitchToBranch, additionalToolbarItems, messageRenderer, toolbar, copyButton, editButton, branchNavigation, children, className, ...props }: CopilotChatUserMessageProps) { const flattenedContent = useMemo( () => flattenUserMessageContent(message.content), [message.content], ); const mediaParts = useMemo( () => getMediaParts(message.content), [message.content], ); const BoundMessageRenderer = renderSlot( messageRenderer, CopilotChatUserMessage.MessageRenderer, { content: flattenedContent, }, ); const BoundCopyButton = renderSlot( copyButton, CopilotChatUserMessage.CopyButton, { onClick: async () => { if (flattenedContent) { return await copyToClipboard(flattenedContent); } return false; }, }, ); const BoundEditButton = renderSlot( editButton, CopilotChatUserMessage.EditButton, { onClick: () => onEditMessage?.({ message }), }, ); const BoundBranchNavigation = renderSlot( branchNavigation, CopilotChatUserMessage.BranchNavigation, { currentBranch: branchIndex, numberOfBranches, onSwitchToBranch, message, }, ); const showBranchNavigation = numberOfBranches && numberOfBranches > 1 && onSwitchToBranch; const BoundToolbar = renderSlot(toolbar, CopilotChatUserMessage.Toolbar, { children: (
{additionalToolbarItems} {BoundCopyButton} {onEditMessage && BoundEditButton} {showBranchNavigation && BoundBranchNavigation}
), }); if (children) { return (
{children({ messageRenderer: BoundMessageRenderer, toolbar: BoundToolbar, copyButton: BoundCopyButton, editButton: BoundEditButton, branchNavigation: BoundBranchNavigation, message, branchIndex, numberOfBranches, additionalToolbarItems, })}
); } return (
{mediaParts.length > 0 && (
{mediaParts.map((part, index) => ( ))}
)} {BoundMessageRenderer} {BoundToolbar}
); } // eslint-disable-next-line @typescript-eslint/no-namespace export namespace CopilotChatUserMessage { export const Container: React.FC< React.PropsWithChildren> > = ({ children, className, ...props }) => (
{children}
); export const MessageRenderer: React.FC<{ content: string; className?: string; }> = ({ content, className }) => (
{content}
); export const Toolbar: React.FC> = ({ className, ...props }) => (
); export const ToolbarButton: React.FC< React.ButtonHTMLAttributes & { title: string; children: React.ReactNode; } > = ({ title, children, className, ...props }) => { return (

{title}

); }; export const CopyButton: React.FC< React.ButtonHTMLAttributes & { copied?: boolean } > = ({ className, title, onClick, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; const [copied, setCopied] = useState(false); 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); setTimeout(() => setCopied(false), 2000); } }; return ( {copied ? ( ) : ( )} ); }; export const EditButton: React.FC< React.ButtonHTMLAttributes > = ({ className, title, ...props }) => { const config = useCopilotChatConfiguration(); const labels = config?.labels ?? CopilotChatDefaultLabels; return ( ); }; export const BranchNavigation: React.FC< React.HTMLAttributes & { currentBranch?: number; numberOfBranches?: number; onSwitchToBranch?: ( props: CopilotChatUserMessageOnSwitchToBranchProps, ) => void; message: UserMessage; } > = ({ className, currentBranch = 0, numberOfBranches = 1, onSwitchToBranch, message, ...props }) => { if (!numberOfBranches || numberOfBranches <= 1 || !onSwitchToBranch) { return null; } const canGoPrev = currentBranch > 0; const canGoNext = currentBranch < numberOfBranches - 1; return (
{currentBranch + 1}/{numberOfBranches}
); }; } CopilotChatUserMessage.Container.displayName = "CopilotChatUserMessage.Container"; CopilotChatUserMessage.MessageRenderer.displayName = "CopilotChatUserMessage.MessageRenderer"; CopilotChatUserMessage.Toolbar.displayName = "CopilotChatUserMessage.Toolbar"; CopilotChatUserMessage.ToolbarButton.displayName = "CopilotChatUserMessage.ToolbarButton"; CopilotChatUserMessage.CopyButton.displayName = "CopilotChatUserMessage.CopyButton"; CopilotChatUserMessage.EditButton.displayName = "CopilotChatUserMessage.EditButton"; CopilotChatUserMessage.BranchNavigation.displayName = "CopilotChatUserMessage.BranchNavigation"; export default CopilotChatUserMessage;