Template
122 lines
4.0 KiB
TypeScript
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>
|
|
);
|
|
}
|