"use client"; import React, { useMemo, useState } from "react"; import { cva, type VariantProps } from "class-variance-authority"; import { motion } from "framer-motion"; import { Ban, ChevronRight, Code2, Loader2, Terminal } from "lucide-react"; import { cn } from "@/lib/utils"; import { Collapsible, CollapsibleContent, CollapsibleTrigger, } from "@/components/ui/collapsible"; import { FilePreview } from "@/components/ui/file-preview"; import { MarkdownRenderer } from "@/components/chat-playground/markdown-renderer"; const chatBubbleVariants = cva( "group/message relative break-words rounded-lg p-3 text-sm sm:max-w-[70%]", { variants: { isUser: { true: "bg-primary text-primary-foreground", false: "bg-muted text-foreground", }, animation: { none: "", slide: "duration-300 animate-in fade-in-0", scale: "duration-300 animate-in fade-in-0 zoom-in-75", fade: "duration-500 animate-in fade-in-0", }, }, compoundVariants: [ { isUser: true, animation: "slide", class: "slide-in-from-right", }, { isUser: false, animation: "slide", class: "slide-in-from-left", }, { isUser: true, animation: "scale", class: "origin-bottom-right", }, { isUser: false, animation: "scale", class: "origin-bottom-left", }, ], } ); type Animation = VariantProps["animation"]; interface Attachment { name?: string; contentType?: string; url: string; } interface PartialToolCall { state: "partial-call"; toolName: string; } interface ToolCall { state: "call"; toolName: string; } interface ToolResult { state: "result"; toolName: string; result: { __cancelled?: boolean; [key: string]: unknown; }; } type ToolInvocation = PartialToolCall | ToolCall | ToolResult; interface ReasoningPart { type: "reasoning"; reasoning: string; } interface ToolInvocationPart { type: "tool-invocation"; toolInvocation: ToolInvocation; } interface TextPart { type: "text"; text: string; } // For compatibility with AI SDK types, not used interface SourcePart { type: "source"; source?: unknown; } interface FilePart { type: "file"; mimeType: string; data: string; } interface StepStartPart { type: "step-start"; } type MessagePart = | TextPart | ReasoningPart | ToolInvocationPart | SourcePart | FilePart | StepStartPart; export interface Message { id: string; role: "user" | "assistant" | (string & {}); content: string; createdAt?: Date; experimental_attachments?: Attachment[]; toolInvocations?: ToolInvocation[]; parts?: MessagePart[]; } export interface ChatMessageProps extends Message { showTimeStamp?: boolean; animation?: Animation; actions?: React.ReactNode; } export const ChatMessage: React.FC = ({ role, content, createdAt, showTimeStamp = false, animation = "scale", actions, experimental_attachments, toolInvocations, parts, }) => { const files = useMemo(() => { return experimental_attachments?.map(attachment => { const dataArray = dataUrlToUint8Array(attachment.url); const file = new File([dataArray], attachment.name ?? "Unknown", { type: attachment.contentType, }); return file; }); }, [experimental_attachments]); const isUser = role === "user"; const formattedTime = createdAt?.toLocaleTimeString("en-US", { hour: "2-digit", minute: "2-digit", }); if (isUser) { return (
{files ? (
{files.map((file, index) => { return ; })}
) : null}
{content}
{showTimeStamp && createdAt ? ( ) : null}
); } if (parts && parts.length > 0) { return parts.map((part, index) => { if (part.type === "text") { return (
{part.text} {actions ? (
{actions}
) : null}
{showTimeStamp && createdAt ? ( ) : null}
); } else if (part.type === "reasoning") { return ; } else if (part.type === "tool-invocation") { return ( ); } return null; }); } if (toolInvocations && toolInvocations.length > 0) { return ; } return (
{content} {actions ? (
{actions}
) : null}
{showTimeStamp && createdAt ? ( ) : null}
); }; function dataUrlToUint8Array(data: string) { const base64 = data.split(",")[1]; const buf = Buffer.from(base64, "base64"); return new Uint8Array(buf); } const ReasoningBlock = ({ part }: { part: ReasoningPart }) => { const [isOpen, setIsOpen] = useState(false); return (
{part.reasoning}
); }; function ToolCall({ toolInvocations, }: Pick) { if (!toolInvocations?.length) return null; return (
{toolInvocations.map((invocation, index) => { const isCancelled = invocation.state === "result" && invocation.result.__cancelled === true; if (isCancelled) { return (
Cancelled{" "} {"`"} {invocation.toolName} {"`"}
); } switch (invocation.state) { case "partial-call": case "call": return (
Calling{" "} {"`"} {invocation.toolName} {"`"} ...
); case "result": return (
Result from{" "} {"`"} {invocation.toolName} {"`"}
                  {JSON.stringify(invocation.result, null, 2)}
                
); default: return null; } })}
); }