"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]: any } } 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?: any } 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 } })}
) }