Progress and spoken text shared a 24px consultation-report gap, and the jump chip covered option D. Put D-chart names back on the activity strip from evidence rescore methods. Co-authored-by: Cursor <cursoragent@cursor.com>
181 lines
6.3 KiB
TypeScript
181 lines
6.3 KiB
TypeScript
"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 { ConsultationThinkingReport } from "@/components/consultation-thinking-report";
|
|
import type { ChatMessageView } from "@/lib/chat-message-view";
|
|
import { useEffect, useLayoutEffect, useRef } from "react";
|
|
|
|
type GsapCore = typeof import("gsap")["gsap"];
|
|
|
|
const useEntryEffect = typeof window === "undefined" ? useEffect : useLayoutEffect;
|
|
|
|
let gsapCore: GsapCore | undefined;
|
|
let gsapRequest: Promise<GsapCore> | 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 function AgentAvatar() {
|
|
return <span className="agent-avatar" aria-hidden="true" />;
|
|
}
|
|
|
|
export function ChatMessageRow({
|
|
message,
|
|
showActivity = message.state !== "settled",
|
|
vargaSentence,
|
|
}: Readonly<{
|
|
message: ChatMessageView;
|
|
showActivity?: boolean;
|
|
vargaSentence?: string | null;
|
|
}>) {
|
|
const messageRow = useRef<HTMLElement>(null);
|
|
const assistantLabel = message.state === "thinking"
|
|
? "Jyotisha 正在分析"
|
|
: message.state === "streaming"
|
|
? "Jyotisha 正在回答"
|
|
: "Jyotisha";
|
|
const activityState = message.activity
|
|
? ({
|
|
"loading-method": "searching",
|
|
"chart-calculation": "solving",
|
|
"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 = message.timeline;
|
|
const thinkingSections = message.thinkingSections ?? [];
|
|
const hasTrace = (message.activityTrace?.length ?? 0) > 0;
|
|
const showReport = consultTimeline === undefined && thinkingSections.length > 0;
|
|
const showLiveActivity = showActivity && !showReport && consultTimeline === undefined;
|
|
const showThinkingPanel = showLiveActivity || (!showReport && consultTimeline === undefined && (Boolean(message.thinkingText?.trim()) || hasTrace));
|
|
const showSpokenAnswer = !showReport && 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.18,
|
|
ease: "cubic-bezier(.22, 1, .36, 1)",
|
|
y: 0,
|
|
});
|
|
});
|
|
return () => motion.revert();
|
|
}, [message.role]);
|
|
|
|
const thinkingPanel = showThinkingPanel
|
|
? (
|
|
<AgentActivityStatus
|
|
state={activityState}
|
|
label={activityLabel}
|
|
startedAt={message.activity?.startedAt}
|
|
completedTrail={message.activity?.completedTrail}
|
|
thinkingText={hasTrace ? undefined : message.thinkingText}
|
|
activityTrace={message.activityTrace}
|
|
vargaSentence={vargaSentence}
|
|
hasAnswer={hasAnswer}
|
|
showLive={showLiveActivity}
|
|
/>
|
|
)
|
|
: null;
|
|
const spokenAnswer = showSpokenAnswer || (consultTimeline !== undefined && hasAnswer)
|
|
? (
|
|
<ChatMessageContent
|
|
text={message.text}
|
|
auditRows={message.agentExecutionReceipt?.techniqueAuditTable}
|
|
vargaSentence={showThinkingPanel ? null : vargaSentence}
|
|
/>
|
|
)
|
|
: null;
|
|
|
|
return (
|
|
<article
|
|
ref={messageRow}
|
|
className={`message message-${message.role}`}
|
|
aria-label={message.role === "assistant" ? assistantLabel : "你"}
|
|
>
|
|
{message.role === "assistant" && <AgentAvatar />}
|
|
<div className="message-content">
|
|
<div className="message-bubble">
|
|
{message.role === "assistant" ? (
|
|
consultTimeline !== undefined ? (
|
|
<div className="consultation-thinking-report">
|
|
<ConsultationRunTimeline
|
|
rows={consultTimeline}
|
|
live={showActivity && message.state !== "settled"}
|
|
/>
|
|
{hasAnswer ? (
|
|
<section className="consultation-report-analysis" aria-label="回复">
|
|
{spokenAnswer}
|
|
</section>
|
|
) : null}
|
|
</div>
|
|
) : (
|
|
<>
|
|
{showReport && (
|
|
<ConsultationThinkingReport
|
|
sections={thinkingSections}
|
|
thinkingText={message.thinkingText}
|
|
answer={message.text}
|
|
live={showActivity && !hasAnswer}
|
|
liveLabel={activityLabel}
|
|
liveState={activityState}
|
|
startedAt={message.activity?.startedAt}
|
|
auditRows={message.agentExecutionReceipt?.techniqueAuditTable}
|
|
vargaSentence={vargaSentence}
|
|
/>
|
|
)}
|
|
{stackedThinkingAndAnswer ? (
|
|
<div className="message-stage-and-answer">
|
|
{thinkingPanel}
|
|
<section className="consultation-report-analysis" aria-label="回复">
|
|
{spokenAnswer}
|
|
</section>
|
|
</div>
|
|
) : (
|
|
<>
|
|
{thinkingPanel}
|
|
{spokenAnswer}
|
|
</>
|
|
)}
|
|
</>
|
|
)
|
|
) : <p>{message.text}</p>}
|
|
</div>
|
|
</div>
|
|
</article>
|
|
);
|
|
}
|