"use client"; import { AgentActivityStatus } from "@/components/agent-activity-status"; import { prefetchOnIdle } from "@/components/chat-chunk-prefetch"; import { ChatMessageContent } from "@/components/chat-message-content"; import { ConsultationRunTimeline } from "@/components/consultation-run-timeline"; import type { ChatMessageView } from "@/lib/chat-message-view"; import { memo, useEffect, useLayoutEffect, useRef, type ReactNode } from "react"; type GsapCore = typeof import("gsap")["gsap"]; const useEntryEffect = typeof window === "undefined" ? useEffect : useLayoutEffect; let gsapCore: GsapCore | undefined; let gsapRequest: Promise | undefined; function loadGsap() { gsapRequest ??= import("gsap").then((module) => { const core = module.gsap ?? module.default; gsapCore = (core as GsapCore & { gsap?: GsapCore }).gsap ?? core; return gsapCore; }); return gsapRequest; } function motionPreferred() { return typeof window !== "undefined" && typeof window.matchMedia === "function" && !window.matchMedia("(prefers-reduced-motion: reduce)").matches; } if (motionPreferred()) prefetchOnIdle(loadGsap); export const ChatMessageRow = memo(function ChatMessageRow({ message, showActivity = message.state !== "settled", vargaSentence, afterAnswer, stoppedNotice, }: Readonly<{ message: ChatMessageView; showActivity?: boolean; vargaSentence?: string | null; afterAnswer?: ReactNode; stoppedNotice?: string; }>) { const messageRow = useRef(null); const smalltalk = message.responseKind === "smalltalk"; // Only a smalltalk reply is quiet. The wait before the server's first event // used to be quiet too, so nothing showed for the classification round trip // (BUG-1074); now the timeline's queued row is on screen from the send frame // and disappears when the run.completed event says the reply was smalltalk. const quiet = smalltalk; const assistantLabel = quiet ? "Jyotisha" : message.state === "thinking" ? "Jyotisha 正在分析" : message.state === "streaming" ? "Jyotisha 正在回答" : "Jyotisha"; const activityState = message.activity ? ({ received: "working", "loading-method": "searching", "chart-calculation": "solving", "compute-queue": "working", "evidence-validation": "working", "answer-composition": "composing", } as const)[message.activity.phase] : message.state === "thinking" ? "working" : "composing"; const activityLabel = message.activity?.label ?? (message.state === "thinking" ? "正在处理…" : undefined); const hasAnswer = Boolean(message.text.trim()); const consultTimeline = quiet ? undefined : message.timeline; // Rows with a step timeline (every consultation and rectification reply) render // `ConsultationRunTimeline`; the activity panel is the fallback for rows without one. const showLiveActivity = !quiet && showActivity && consultTimeline === undefined; const showThinkingPanel = !quiet && (showLiveActivity || (consultTimeline === undefined && Boolean(message.thinkingText?.trim()))); const showSpokenAnswer = Boolean(message.text); const stackedThinkingAndAnswer = showThinkingPanel && showSpokenAnswer; useEntryEffect(() => { const row = messageRow.current; if (!row) return; if (!gsapCore) { if (motionPreferred()) void loadGsap(); return; } const gsap = gsapCore; const motion = gsap.matchMedia(); motion.add("(prefers-reduced-motion: no-preference)", () => { gsap.fromTo(row, { autoAlpha: 0, y: message.role === "user" ? 8 : 12, }, { autoAlpha: 1, clearProps: "opacity,transform,visibility", duration: 0.16, ease: "cubic-bezier(.22, 1, .36, 1)", y: 0, }); }); return () => motion.revert(); }, [message.role]); const thinkingPanel = showThinkingPanel ? ( ) : null; const spokenAnswer = showSpokenAnswer || (consultTimeline !== undefined && hasAnswer) ? ( ) : null; return (
{message.role === "assistant" ? ( consultTimeline !== undefined ? (
{hasAnswer ? (
{spokenAnswer} {afterAnswer}
) : afterAnswer}
) : ( <> {stackedThinkingAndAnswer ? (
{thinkingPanel}
{spokenAnswer} {afterAnswer}
) : ( <> {thinkingPanel} {spokenAnswer} {afterAnswer} )} ) ) : <>

{message.text}

{afterAnswer}} {stoppedNotice ?

{stoppedNotice}

: null}
); });