"use client"; import { ChatMessageRow } from "@/components/chat-message-row"; import { ChatMessageActions, toggleChatMessageFeedback, type ChatMessageFeedback, } from "@/components/chat-message-actions"; import { ConversationFollowUps } from "@/components/conversation-follow-ups"; import { isGeneralDailyFortuneQuestion } from "@/lib/consultation-entrypoint"; import { deriveConsultationFollowUps } from "@/lib/consultation-follow-ups"; import type { ConsultationDomain } from "@/lib/consultation-domain-registry"; import type { AgentActivityView, ChatMessage, ChatMessageView } from "@/lib/chat-message-view"; import { chatMessageViews, latestAssistantView, settledChatMessageViews, } from "@/lib/chat-message-view"; import type { PublicThinkingSection } from "@/lib/consultation-thinking-plan"; import type { ConsultationTimelineRow } from "@/lib/consultation-run-timeline"; import { noteLatestEntryMount, noteSettledListRender, noteSettledRowRender, noteStreamingRowRender, noteUnsplitListRender, } from "@/lib/home-streaming-render-probe"; import { memo, useEffect, type MutableRefObject } from "react"; export type ChatTranscriptActions = Readonly<{ onFeedback: (feedbackKey: string, requested: ChatMessageFeedback) => void; onCopy: (feedbackKey: string, text: string) => void; onRegenerate: (renderKey: string) => void; onFollowUp: (question: string) => void; }>; export type ChatTranscriptProps = Readonly<{ messages: readonly ChatMessage[]; loading: boolean; streamingText: string; streamingActivity?: AgentActivityView; streamingThinking?: string; streamingSections?: readonly PublicThinkingSection[]; streamingTimeline?: readonly ConsultationTimelineRow[]; sessionId: string; sessionType: "consultation" | "birth_time_rectification"; theme?: ConsultationDomain; messageFeedback: Readonly>; copiedMessageKey: string | null; cancellationPending: boolean; productEntrypointsDisabled: boolean; actionsRef: MutableRefObject; }>; type MessageEntryProps = Readonly<{ message: ChatMessageView; views: readonly ChatMessageView[]; index: number; sessionId: string; sessionType: "consultation" | "birth_time_rectification"; theme?: ConsultationDomain; messageFeedback: Readonly>; copiedMessageKey: string | null; loading: boolean; cancellationPending: boolean; productEntrypointsDisabled: boolean; actionsRef: MutableRefObject; }>; /** * One transcript row. The same component renders history rows, the row that is * still streaming and the row that just settled: actions and follow-ups appear * once the view is settled, nothing remounts when it does. */ function MessageEntry({ message, views, index, sessionId, sessionType, theme, messageFeedback, copiedMessageKey, loading, cancellationPending, productEntrypointsDisabled, actionsRef, }: MessageEntryProps) { const showActions = message.role === "assistant" && message.state === "settled" && Boolean(message.text); const feedbackKey = `${sessionId}:${message.renderKey}`; const latestRegeneratableKey = !loading && !cancellationPending ? [...views].reverse().find((item) => ( item.role === "assistant" && item.state === "settled" && Boolean(item.text) ))?.renderKey : undefined; const previousQuestion = views[index - 1]?.role === "user" ? views[index - 1]?.text : ""; const followUps = showActions && message.renderKey === latestRegeneratableKey && sessionType === "consultation" && previousQuestion ? deriveConsultationFollowUps({ question: previousQuestion, answer: message.text, theme, entrypoint: isGeneralDailyFortuneQuestion(previousQuestion) ? "daily_starlanguage" : null, }) : []; return (
{showActions && ( actionsRef.current.onFeedback(feedbackKey, requested)} onCopy={() => actionsRef.current.onCopy(feedbackKey, message.text)} onRegenerate={() => actionsRef.current.onRegenerate(message.renderKey)} /> )} actionsRef.current.onFollowUp(question)} />
); } const HistoryMessageEntry = memo(function HistoryMessageEntry(props: MessageEntryProps) { noteSettledRowRender(); return ; }); /** * History rows: every settled message except the trailing assistant reply, * which `LatestAssistantEntry` owns so it keeps one identity from the first * streamed token through settlement. */ export const SettledMessageList = memo(function SettledMessageList({ messages, excludeLatestAssistant = false, sessionId, sessionType, theme, messageFeedback, copiedMessageKey, loading, cancellationPending, productEntrypointsDisabled, actionsRef, }: Omit & { excludeLatestAssistant?: boolean; }) { noteSettledListRender(); const views = settledChatMessageViews(messages); const history = excludeLatestAssistant && views.at(-1)?.role === "assistant" ? views.slice(0, -1) : views; return ( <> {history.map((message, index) => ( ))} ); }); /** The trailing assistant reply, streaming or settled, under one React identity. */ export const LatestAssistantEntry = memo(function LatestAssistantEntry(props: MessageEntryProps) { noteStreamingRowRender(); useEffect(() => { noteLatestEntryMount(); }, []); return ; }); export const ChatTranscript = memo(function ChatTranscript({ messages, loading, streamingText, streamingActivity, streamingThinking, streamingSections, streamingTimeline, sessionId, sessionType, theme, messageFeedback, copiedMessageKey, cancellationPending, productEntrypointsDisabled, actionsRef, }: ChatTranscriptProps) { const latest = latestAssistantView( messages, loading, streamingText, streamingActivity, streamingThinking, streamingSections, streamingTimeline, ); return ( <> {latest ? ( ) : null} ); }); export function UnsplitChatTranscript({ messages, loading, streamingText, streamingActivity, streamingThinking, streamingSections, streamingTimeline, sessionId, sessionType, theme, messageFeedback, copiedMessageKey, cancellationPending, productEntrypointsDisabled, actionsRef, }: ChatTranscriptProps) { noteUnsplitListRender(); const views = chatMessageViews( messages, loading, streamingText, streamingActivity, streamingThinking, streamingSections, streamingTimeline, ); return ( <> {views.map((message, index) => { noteSettledRowRender(); if (message.state !== "settled") noteStreamingRowRender(); return ( ); })} ); } export { toggleChatMessageFeedback };