"use client";
import Image from "next/image";
import React, { useMemo } from "react";
import { AnimatePresence, motion } from "framer-motion";
import {
Avatar,
Box,
IconButton,
Paper,
Stack,
Tooltip,
Typography,
alpha,
useTheme,
} from "@mui/material";
import ContentCopyRounded from "@mui/icons-material/ContentCopyRounded";
import { TbArrowsSplit2 } from "react-icons/tb";
import type { PermissionReply } from "@/lib/chatStream";
import {
parseAssistantMessageSections,
parseContentWithToolCalls,
type ContentSegment,
} from "./chatMessageSections";
import type { Message, SpeechState } from "./GlobalChatbox.types";
import { stripMarkdown } from "./GlobalChatbox.utils";
import { AgentProgressTimeline } from "./AgentProgressTimeline";
import { ChatInlineChart } from "./ChatInlineChart";
import { ChatToolCallBlock } from "./ChatToolCallBlock";
import { MarkdownBlock, normalizeClipboardText } from "./AgentMarkdownBlock";
import { PermissionRequestGroup } from "./AgentPermissionRequests";
import { QuestionRequestGroup } from "./AgentQuestionRequests";
import { TodoPlanCard } from "./AgentTodoPlanCard";
import VolumeUpRounded from "@mui/icons-material/VolumeUpRounded";
import PauseRounded from "@mui/icons-material/PauseRounded";
import PlayArrowRounded from "@mui/icons-material/PlayArrowRounded";
import StopRounded from "@mui/icons-material/StopRounded";
type AgentTurnProps = {
message: Message;
isStreaming: boolean;
messageSpeechState: SpeechState;
onSpeak: (messageId: string, text: string) => void;
onPause: () => void;
onResume: () => void;
onStopSpeech: () => void;
isTtsSupported: boolean;
onCreateBranch: (messageId: string) => void;
onReplyPermission: (requestId: string, reply: PermissionReply) => void;
onReplyQuestion: (requestId: string, answers: string[][]) => void;
onRejectQuestion: (requestId: string) => void;
};
export const AgentTurn = React.memo(
({
message,
isStreaming,
messageSpeechState,
onSpeak,
onPause,
onResume,
onStopSpeech,
isTtsSupported,
onCreateBranch,
onReplyPermission,
onReplyQuestion,
onRejectQuestion,
}: AgentTurnProps) => {
const theme = useTheme();
const isUser = message.role === "user";
const isErrorMessage = Boolean(message.isError);
const [isHovered, setIsHovered] = React.useState(false);
const isProgressComplete = message.progress?.some(
(item) => item.phase === "complete" && item.status === "completed",
) ?? false;
const isProgressRunning = !isErrorMessage && !isProgressComplete && (
message.progress?.some((item) => item.status === "running") ?? false
);
const parsedAssistantSections = useMemo(
() =>
!isUser && !isErrorMessage
? parseAssistantMessageSections(message.content)
: null,
[isErrorMessage, isUser, message.content],
);
const answerContent = parsedAssistantSections?.answer ?? message.content;
const contentSegments: ContentSegment[] = useMemo(
() =>
!isUser && !isErrorMessage
? parseContentWithToolCalls(answerContent).segments
: [{ type: "text", content: answerContent }],
[answerContent, isErrorMessage, isUser],
);
if (isUser) {
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{message.content}
);
}
return (
setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
{message.progress?.length ? (
) : null}
{message.permissions?.length ? (
) : null}
{message.questions?.length ? (
) : null}
{message.todos ? (
) : null}
分析结果
{contentSegments.map((segment, segIdx) => {
if (segment.type === "text") {
const text = segment.content.trim();
if (!text && contentSegments.length > 1) return null;
return {text || "..."};
}
if (segment.type === "tool_call") {
if (
segment.toolCall.tool === "chart" ||
segment.toolCall.tool === "show_chart"
) {
const p = segment.toolCall.params;
return (
);
}
return (
);
}
if (segment.type === "tool_call_pending") {
return (
正在准备工具调用...
);
}
return null;
})}
{isHovered && !isStreaming && (
{
navigator.clipboard.writeText(
normalizeClipboardText(message.content),
);
// Could add a toast here
}}
sx={{ width: 28, height: 28, color: "text.secondary", "&:hover": { color: "#00acc1", bgcolor: alpha("#00acc1", 0.1) } }}
>
{
onCreateBranch(message.id);
}}
sx={{ width: 28, height: 28, color: "text.secondary", "&:hover": { color: "#00acc1", bgcolor: alpha("#00acc1", 0.1) } }}
>
)}
{!isErrorMessage && isTtsSupported ? (
{messageSpeechState === "idle" ? (
onSpeak(message.id, stripMarkdown(answerContent))}
aria-label="朗读消息"
sx={{ color: "text.secondary", opacity: 0.68, p: 0.5 }}
>
) : null}
{messageSpeechState === "playing" ? (
<>
>
) : null}
{messageSpeechState === "paused" ? (
<>
>
) : null}
) : null}
);
},
);
AgentTurn.displayName = "AgentTurn";