feat(chat): 优化分析过程与结果展示
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
|
||||
import Image from "next/image";
|
||||
import React, { useMemo } from "react";
|
||||
import { motion } from "framer-motion";
|
||||
import { motion, useReducedMotion } from "framer-motion";
|
||||
import {
|
||||
Avatar,
|
||||
Box,
|
||||
@@ -33,6 +33,7 @@ import type {
|
||||
import { stripMarkdown } from "./globalChatboxUtils";
|
||||
import { findSpeechSelectionStartOffset } from "./speechStartOptions";
|
||||
import { AgentProgressTimeline } from "./AgentProgressTimeline";
|
||||
import { AgentActivityTimeline } from "./AgentActivityTimeline";
|
||||
import { ChartGenerationSkeleton, ChatInlineChart } from "./ChatInlineChart";
|
||||
import { ChatToolCallBlock } from "./ChatToolCallBlock";
|
||||
import { MarkdownBlock, normalizeClipboardText } from "./AgentMarkdownBlock";
|
||||
@@ -149,61 +150,6 @@ const StreamingStatus = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const StreamingMarkdownBlock = ({
|
||||
text,
|
||||
isStreaming,
|
||||
segmentKey,
|
||||
}: {
|
||||
text: string;
|
||||
isStreaming: boolean;
|
||||
segmentKey: string;
|
||||
}) => {
|
||||
const [streamTextState, setStreamTextState] = React.useState<{
|
||||
displayText: string;
|
||||
animatedTailLength: number;
|
||||
}>({
|
||||
displayText: text,
|
||||
animatedTailLength: 0,
|
||||
});
|
||||
|
||||
React.useLayoutEffect(() => {
|
||||
setStreamTextState((current) => {
|
||||
if (current.displayText === text) {
|
||||
return current;
|
||||
}
|
||||
|
||||
if (!isStreaming) {
|
||||
return {
|
||||
displayText: text,
|
||||
animatedTailLength: 0,
|
||||
};
|
||||
}
|
||||
|
||||
if (current.displayText === text) {
|
||||
return current;
|
||||
}
|
||||
|
||||
return {
|
||||
displayText: text,
|
||||
animatedTailLength:
|
||||
text.length > current.displayText.length &&
|
||||
text.startsWith(current.displayText)
|
||||
? Math.min(48, text.length - current.displayText.length)
|
||||
: 0,
|
||||
};
|
||||
});
|
||||
}, [isStreaming, text]);
|
||||
|
||||
return (
|
||||
<MarkdownBlock
|
||||
streamFadeKey={`${segmentKey}-${streamTextState.displayText.length}`}
|
||||
streamFadeLength={streamTextState.animatedTailLength}
|
||||
>
|
||||
{streamTextState.displayText}
|
||||
</MarkdownBlock>
|
||||
);
|
||||
};
|
||||
|
||||
export const AgentTurn = React.memo(
|
||||
({
|
||||
message,
|
||||
@@ -220,9 +166,11 @@ export const AgentTurn = React.memo(
|
||||
onRejectQuestion,
|
||||
}: AgentTurnProps) => {
|
||||
const theme = useTheme();
|
||||
const reduceMotion = useReducedMotion();
|
||||
const isUser = message.role === "user";
|
||||
const isErrorMessage = Boolean(message.isError);
|
||||
const isStreamingAssistant = !isUser && !isErrorMessage && isStreaming;
|
||||
const hasFinalAnswer = message.content.trim().length > 0;
|
||||
const [isHovered, setIsHovered] = React.useState(false);
|
||||
const answerContentRef = React.useRef<HTMLDivElement | null>(null);
|
||||
const [speechSelection, setSpeechSelection] = React.useState<SpeechSelection | null>(null);
|
||||
@@ -230,7 +178,8 @@ export const AgentTurn = React.memo(
|
||||
(item) => item.phase === "complete" && item.status === "completed",
|
||||
) ?? false;
|
||||
const isProgressRunning = !isErrorMessage && !isProgressComplete && (
|
||||
message.progress?.some((item) => item.status === "running") ?? false
|
||||
(message.activities?.some((item) => item.status === "running") ?? false) ||
|
||||
(message.progress?.some((item) => item.status === "running") ?? false)
|
||||
);
|
||||
|
||||
const parsedAssistantSections = useMemo(
|
||||
@@ -456,7 +405,9 @@ export const AgentTurn = React.memo(
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.5}>
|
||||
{message.progress?.length ? (
|
||||
{message.activities?.length ? (
|
||||
<AgentActivityTimeline activities={message.activities} />
|
||||
) : message.progress?.length ? (
|
||||
<AgentProgressTimeline progress={message.progress} isAborted={isErrorMessage} />
|
||||
) : null}
|
||||
|
||||
@@ -493,63 +444,98 @@ export const AgentTurn = React.memo(
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.2}>
|
||||
<Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
|
||||
<Typography variant="caption" color="text.secondary" fontWeight={800} sx={{ letterSpacing: 0.5 }}>
|
||||
<Stack
|
||||
direction="row"
|
||||
alignItems="center"
|
||||
justifyContent="space-between"
|
||||
spacing={1}
|
||||
>
|
||||
<Typography
|
||||
variant="caption"
|
||||
color="text.secondary"
|
||||
fontWeight={800}
|
||||
sx={{ letterSpacing: 0.5 }}
|
||||
>
|
||||
分析结果
|
||||
</Typography>
|
||||
{isStreamingAssistant ? <StreamingStatus /> : null}
|
||||
</Stack>
|
||||
{contentSegments.map((segment, segIdx) => {
|
||||
if (segment.type === "text") {
|
||||
const text = segment.content.trim();
|
||||
if (!text && contentSegments.length > 1) return null;
|
||||
return (
|
||||
<StreamingMarkdownBlock
|
||||
key={segIdx}
|
||||
text={text || "..."}
|
||||
isStreaming={isStreamingAssistant}
|
||||
segmentKey={`${message.id}-${segIdx}`}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call") {
|
||||
if (
|
||||
segment.toolCall.tool === "chart" ||
|
||||
segment.toolCall.tool === "show_chart"
|
||||
) {
|
||||
const p = segment.toolCall.params;
|
||||
return (
|
||||
<ChatInlineChart
|
||||
key={segment.toolCall.id}
|
||||
title={(p.title as string) ?? undefined}
|
||||
chart_type={
|
||||
(p.chart_type as "line" | "bar" | "pie") ?? "line"
|
||||
}
|
||||
x_data={p.x_data ?? p.xData ?? p.labels ?? p.categories}
|
||||
series={p.series}
|
||||
x_axis_name={(p.x_axis_name as string) ?? undefined}
|
||||
y_axis_name={(p.y_axis_name as string) ?? undefined}
|
||||
isStreaming={isStreamingAssistant}
|
||||
/>
|
||||
);
|
||||
{hasFinalAnswer || !isStreamingAssistant ? (
|
||||
<motion.div
|
||||
data-testid="agent-answer-content"
|
||||
initial={
|
||||
reduceMotion
|
||||
? false
|
||||
: { opacity: 0, y: 6, filter: "blur(2px)" }
|
||||
}
|
||||
return (
|
||||
<ChatToolCallBlock
|
||||
key={segment.toolCall.id}
|
||||
toolCall={segment.toolCall}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call_pending") {
|
||||
return (
|
||||
<ChartGenerationSkeleton
|
||||
key="tool-pending"
|
||||
status={<StreamingStatus />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
animate={{ opacity: 1, y: 0, filter: "blur(0px)" }}
|
||||
transition={{
|
||||
duration: reduceMotion ? 0 : 0.24,
|
||||
ease: [0.16, 1, 0.3, 1],
|
||||
}}
|
||||
>
|
||||
<Stack spacing={1.2}>
|
||||
{contentSegments.map((segment, segIdx) => {
|
||||
if (segment.type === "text") {
|
||||
const text = segment.content.trim();
|
||||
if (!text && contentSegments.length > 1) return null;
|
||||
return (
|
||||
<MarkdownBlock key={segIdx}>
|
||||
{text || "..."}
|
||||
</MarkdownBlock>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call") {
|
||||
if (
|
||||
segment.toolCall.tool === "chart" ||
|
||||
segment.toolCall.tool === "show_chart"
|
||||
) {
|
||||
const p = segment.toolCall.params;
|
||||
return (
|
||||
<ChatInlineChart
|
||||
key={segment.toolCall.id}
|
||||
title={(p.title as string) ?? undefined}
|
||||
chart_type={
|
||||
(p.chart_type as "line" | "bar" | "pie") ??
|
||||
"line"
|
||||
}
|
||||
x_data={
|
||||
p.x_data ??
|
||||
p.xData ??
|
||||
p.labels ??
|
||||
p.categories
|
||||
}
|
||||
series={p.series}
|
||||
x_axis_name={
|
||||
(p.x_axis_name as string) ?? undefined
|
||||
}
|
||||
y_axis_name={
|
||||
(p.y_axis_name as string) ?? undefined
|
||||
}
|
||||
isStreaming={isStreamingAssistant}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<ChatToolCallBlock
|
||||
key={segment.toolCall.id}
|
||||
toolCall={segment.toolCall}
|
||||
/>
|
||||
);
|
||||
}
|
||||
if (segment.type === "tool_call_pending") {
|
||||
return (
|
||||
<ChartGenerationSkeleton
|
||||
key="tool-pending"
|
||||
status={<StreamingStatus />}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
</Stack>
|
||||
</motion.div>
|
||||
) : null}
|
||||
</Stack>
|
||||
</Box>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user