Files
nextjs-chat-gpt/components/chat/ChatMessages.tsx
T

122 lines
4.0 KiB
TypeScript

"use client";
import { isToolUIPart, type ChatStatus, type UIMessage } from "ai";
import {
Conversation,
ConversationContent,
ConversationScrollButton,
} from "@/components/ai-elements/conversation";
import {
Message,
MessageAttachment,
MessageAttachments,
MessageContent,
MessageResponse,
} from "@/components/ai-elements/message";
import {
Reasoning,
ReasoningContent,
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Loader } from "@/components/ai-elements/loader";
import { ToolPartRenderer } from "@/components/tools/tool-part-renderer";
import { getTextContent, getReasoningPart } from "@/lib/chat/messages";
interface ChatMessagesProps {
messages: UIMessage[];
status?: ChatStatus;
}
function LoadingIndicator() {
return (
<Message from="assistant">
<MessageContent from="assistant">
<Loader />
</MessageContent>
</Message>
);
}
export function ChatMessages({ messages, status = "ready" }: ChatMessagesProps) {
const hasReasoningPart = (parts: typeof messages[0]["parts"]) =>
parts.some((p) => p.type === "reasoning" && p.text);
const visibleMessages = messages.filter(
(message) =>
message.role === "user" ||
getTextContent(message.parts).trim() !== "" ||
message.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
hasReasoningPart(message.parts)
);
const lastMessage = messages[messages.length - 1];
const hasContent =
getTextContent(lastMessage?.parts || []).trim() !== "" ||
lastMessage?.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
hasReasoningPart(lastMessage?.parts || []);
const isLoading = status === "submitted" || status === "streaming";
const showLoading =
isLoading && !(lastMessage?.role === "assistant" && hasContent);
return (
<Conversation>
<ConversationContent className="mx-auto max-w-3xl px-4 pb-4">
{visibleMessages.map((message) => {
const isStreaming =
status === "streaming" &&
message.role === "assistant" &&
message.id === lastMessage?.id;
const textContent = getTextContent(message.parts);
const reasoningPart = getReasoningPart(message.parts);
const fileParts = message.parts.filter((p) => p.type === "file");
const toolParts = message.parts.filter((p) => isToolUIPart(p));
return (
<Message key={message.id} from={message.role}>
{fileParts.length > 0 && (
<MessageAttachments>
{fileParts.map((part, index) => (
<MessageAttachment
data={part}
key={`${message.id}-file-${index}`}
/>
))}
</MessageAttachments>
)}
{message.role === "assistant" && reasoningPart && (
<Reasoning
isStreaming={isStreaming && reasoningPart.state === "streaming"}
>
<ReasoningTrigger />
<ReasoningContent>{reasoningPart.text || ""}</ReasoningContent>
</Reasoning>
)}
{toolParts.map((part, index) => (
<ToolPartRenderer key={`tool-${index}`} part={part} />
))}
{textContent && (
<MessageContent from={message.role}>
<MessageResponse
mode={message.role === "assistant" ? "streaming" : "static"}
isAnimating={
isStreaming && message.parts.some(
(part) => part.type === "text" && part.state === "streaming"
)
}
>
{textContent}
</MessageResponse>
</MessageContent>
)}
</Message>
);
})}
{showLoading && <LoadingIndicator />}
</ConversationContent>
<ConversationScrollButton />
</Conversation>
);
}