"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)} > TJWater Agent {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";