"use client"; import type { Message as TMessage } from "ai"; import { memo, useCallback, useEffect, useState } from "react"; import equal from "fast-deep-equal"; import { Markdown } from "./markdown"; import { cn } from "@/lib/utils"; import { ChevronDownIcon, ChevronUpIcon, LightbulbIcon, BrainIcon } from "lucide-react"; import { SpinnerIcon } from "./icons"; import { ToolInvocation } from "./tool-invocation"; import { CopyButton } from "./copy-button"; interface ReasoningPart { type: "reasoning"; reasoning: string; details: Array<{ type: "text"; text: string }>; } interface ReasoningMessagePartProps { part: ReasoningPart; isReasoning: boolean; } interface ThinkingPart { type: "thinking"; thinking: string; details: Array<{ type: "text"; text: string }>; } interface ThinkingMessagePartProps { part: ThinkingPart; isThinking: boolean; } export function ThinkingMessagePart({ part, isThinking, }: ThinkingMessagePartProps) { const [isExpanded, setIsExpanded] = useState(false); const memoizedSetIsExpanded = useCallback((value: boolean) => { setIsExpanded(value); }, []); useEffect(() => { memoizedSetIsExpanded(isThinking); }, [isThinking, memoizedSetIsExpanded]); return (
{isThinking ? (
Thinking...
) : ( )} {isExpanded && (
{part.details.map((detail, detailIndex) => detail.type === "text" ? (
{detail.text}
) : ( "" ), )}
)}
); } export function ReasoningMessagePart({ part, isReasoning, }: ReasoningMessagePartProps) { const [isExpanded, setIsExpanded] = useState(false); const memoizedSetIsExpanded = useCallback((value: boolean) => { setIsExpanded(value); }, []); useEffect(() => { memoizedSetIsExpanded(isReasoning); }, [isReasoning, memoizedSetIsExpanded]); return (
{isReasoning ? (
Thinking...
) : ( )} {isExpanded && (
The assistant's thought process:
{part.details.map((detail, detailIndex) => detail.type === "text" ? (
{detail.text}
) : ( "" ), )}
)}
); } const PurePreviewMessage = ({ message, isLatestMessage, status, }: { message: TMessage; isLoading: boolean; status: "error" | "submitted" | "streaming" | "ready"; isLatestMessage: boolean; }) => { // Create a string with all text parts for copy functionality const getMessageText = () => { if (!message.parts) return ""; return message.parts .filter((part) => part.type === "text") .map((part) => (part.type === "text" ? part.text : "")) .join("\n\n"); }; // Only show copy button if the message is from the assistant and not currently streaming const shouldShowCopyButton = message.role === "assistant" && (!isLatestMessage || status !== "streaming"); return (
{message.content && message.content .split(/([\s\S]*?(?:<\/think>|$))/g) .map((part: string, i: number) => { if (part.startsWith("")) { const isClosed = part.endsWith(""); const thinkingContent = part.slice( 7, isClosed ? -8 : undefined, ); return ( ); } else if (part.trim()) { return (
{part}
); } return null; })} {shouldShowCopyButton && (
)}
); }; export const Message = memo(PurePreviewMessage, (prevProps, nextProps) => { if (prevProps.status !== nextProps.status) return false; if (prevProps.isLoading !== nextProps.isLoading) return false; if (prevProps.isLatestMessage !== nextProps.isLatestMessage) return false; if (prevProps.message.annotations !== nextProps.message.annotations) return false; if (prevProps.message.id !== nextProps.message.id) return false; if (!equal(prevProps.message.parts, nextProps.message.parts)) return false; return true; });