All files / components/transcript index.tsx

78.57% Statements 11/14
36.84% Branches 7/19
25% Functions 1/4
78.57% Lines 11/14

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 841x 1x   1x 1x 1x   1x                               10x                         10x   10x       10x     10x                                                                            
import { type UIMessage, isToolUIPart } from "ai";
import { getToolParts, getMutationSuccessCard } from "./tool";
import { type RefObject } from "react";
import { LoadingIndicator } from "@/components/chat/loading-indicator";
import { ChatEmptyState, SuggestionChips } from "@/components/chat/empty-state";
import { CHAT_TRANSCRIPT_COPY } from "@/constants/chat";
import type { Suggestion } from "@/types/chat";
import { ChatMessage } from "./message";
 
type ChatTranscriptProps = {
  containerRef: RefObject<HTMLDivElement | null>;
  messages: UIMessage[];
  isLoading: boolean;
  userAvatarUrl?: string;
  userAvatarLabel?: string;
  userInitials?: string;
  suggestions: Suggestion[];
  messageFiles?: Map<string, File>;
  onSelectPrompt: (prompt: string) => void;
  onToolApproval: (id: string, approved: boolean) => void;
  onFilePreview?: (file: File) => void;
};
 
export function ChatTranscript({
  containerRef,
  messages,
  isLoading,
  userAvatarUrl,
  userAvatarLabel = "User avatar",
  userInitials = CHAT_TRANSCRIPT_COPY.userBadge,
  suggestions,
  messageFiles,
  onSelectPrompt,
  onToolApproval,
  onFilePreview,
}: ChatTranscriptProps) {
  const lastMessage = messages.at(-1);
  const hasApprovalResponded =
    !isLoading &&
    lastMessage?.role === "assistant" &&
    lastMessage.parts.some((p) => isToolUIPart(p) && p.state === "approval-responded");
  const hasMutationSuccess =
    !isLoading &&
    lastMessage?.role === "assistant" &&
    getToolParts(lastMessage).some((p) => getMutationSuccessCard(p) !== null);
  const showSuggestions = (hasApprovalResponded || hasMutationSuccess) && suggestions.length > 0;
 
  return (
    <div ref={containerRef} className="min-h-0 flex-1 overflow-y-auto px-4 py-6 sm:px-6 lg:px-8">
      {messages.length === 0 ? (
        <ChatEmptyState suggestions={suggestions} onSelectPrompt={onSelectPrompt} />
      ) : (
        <div className="mx-auto flex w-full max-w-3xl flex-col gap-8">
          {messages.map((message) => (
            <ChatMessage
              key={message.id}
              message={message}
              isLastMessage={message.id === lastMessage?.id}
              isLoading={isLoading}
              userAvatarUrl={userAvatarUrl}
              userAvatarLabel={userAvatarLabel}
              userInitials={userInitials}
              attachedFile={messageFiles?.get(message.id)}
              onSelectPrompt={onSelectPrompt}
              onToolApproval={onToolApproval}
              onFilePreview={onFilePreview}
            />
          ))}
 
          {isLoading && lastMessage?.role === "user" ? (
            <LoadingIndicator label="Thinking" />
          ) : null}
 
          {showSuggestions && (
            <div className="mx-auto w-fit rounded-2xl border border-violet-400/20 bg-violet-500/8 px-5 py-4 backdrop-blur-sm light:border-violet-200 light:bg-violet-50/60">
              <SuggestionChips suggestions={suggestions} onSelectPrompt={onSelectPrompt} />
            </div>
          )}
        </div>
      )}
    </div>
  );
}