"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 ? (
) : (
)}
{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 ? (
) : (
)}
{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 (
);
}
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;
});