Improve chat navigation, model selection, and streaming

This commit is contained in:
Rami Bitar
2026-09-16 13:14:55 -04:00
parent 8790cfb1e2
commit dd04493e25
73 changed files with 1272 additions and 1367 deletions
+28 -13
View File
@@ -1,6 +1,6 @@
"use client";
import type { UIMessage } from "ai";
import { isToolUIPart, type ChatStatus, type UIMessage } from "ai";
import {
Conversation,
ConversationContent,
@@ -19,14 +19,14 @@ import {
ReasoningTrigger,
} from "@/components/ai-elements/reasoning";
import { Loader } from "@/components/ai-elements/loader";
import { ToolPartRenderer, getTextContent, getReasoningPart } from "./ChatMessageParts";
import { ToolPartRenderer } from "@/components/tools/tool-part-renderer";
import { getTextContent, getReasoningPart } from "@/lib/chat/messages";
interface ChatMessagesProps {
messages: UIMessage[];
isLoading?: boolean;
status?: ChatStatus;
}
function LoadingIndicator() {
return (
<Message from="assistant">
@@ -37,8 +37,7 @@ function LoadingIndicator() {
);
}
export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
const isToolPart = (type: string) => type === "file" || type.startsWith("tool-");
export function ChatMessages({ messages, status = "ready" }: ChatMessagesProps) {
const hasReasoningPart = (parts: typeof messages[0]["parts"]) =>
parts.some((p) => p.type === "reasoning" && p.text);
@@ -46,26 +45,31 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
(message) =>
message.role === "user" ||
getTextContent(message.parts).trim() !== "" ||
message.parts.some((p) => isToolPart(p.type)) ||
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) => isToolPart(p.type)) ||
lastMessage?.parts.some((p) => p.type === "file" || isToolUIPart(p)) ||
hasReasoningPart(lastMessage?.parts || []);
const isStreaming = lastMessage?.role === "assistant" && hasContent;
const showLoading = isLoading && !isStreaming;
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) => p.type.startsWith("tool-"));
const toolParts = message.parts.filter((p) => isToolUIPart(p));
return (
<Message key={message.id} from={message.role}>
{fileParts.length > 0 && (
@@ -80,7 +84,9 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
)}
{message.role === "assistant" && reasoningPart && (
<Reasoning isStreaming={reasoningPart.state !== "done"}>
<Reasoning
isStreaming={isStreaming && reasoningPart.state === "streaming"}
>
<ReasoningTrigger />
<ReasoningContent>{reasoningPart.text || ""}</ReasoningContent>
</Reasoning>
@@ -92,7 +98,16 @@ export function ChatMessages({ messages, isLoading }: ChatMessagesProps) {
{textContent && (
<MessageContent from={message.role}>
<MessageResponse>{textContent}</MessageResponse>
<MessageResponse
mode={message.role === "assistant" ? "streaming" : "static"}
isAnimating={
isStreaming && message.parts.some(
(part) => part.type === "text" && part.state === "streaming"
)
}
>
{textContent}
</MessageResponse>
</MessageContent>
)}
</Message>