"use client"; import Image from "next/image"; import React, { useMemo } from "react"; import ReactMarkdown from "react-markdown"; import remarkGfm from "remark-gfm"; import { AnimatePresence, motion } from "framer-motion"; import { Avatar, Box, Button, IconButton, Paper, Stack, Tooltip, Typography, alpha, useTheme, } from "@mui/material"; import ContentCopyRounded from "@mui/icons-material/ContentCopyRounded"; import RefreshRounded from "@mui/icons-material/RefreshRounded"; import EditRounded from "@mui/icons-material/EditRounded"; import CloseRounded from "@mui/icons-material/CloseRounded"; import ChevronLeftRounded from "@mui/icons-material/ChevronLeftRounded"; import ChevronRightRounded from "@mui/icons-material/ChevronRightRounded"; import { parseAssistantMessageSections, parseContentWithToolCalls, type ContentSegment, } from "./chatMessageSections"; import markdownStyles from "./GlobalChatboxMarkdown.module.css"; import type { BranchState, Message, SpeechState } from "./GlobalChatbox.types"; import { stripMarkdown } from "./GlobalChatbox.utils"; import { AgentProgressTimeline } from "./AgentProgressTimeline"; import { ChatInlineChart } from "./ChatInlineChart"; import type { ChatChartSeries } from "./ChatInlineChart"; import { ChatToolCallBlock } from "./ChatToolCallBlock"; import { AgentArtifactPanel } from "./AgentArtifactPanel"; import ErrorOutlineRounded from "@mui/icons-material/ErrorOutlineRounded"; 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"; import SendRounded from "@mui/icons-material/SendRounded"; type AgentTurnProps = { message: Message; branchState?: BranchState; messageSpeechState: SpeechState; onSpeak: (messageId: string, text: string) => void; onPause: () => void; onResume: () => void; onStopSpeech: () => void; isTtsSupported: boolean; onRegenerate: () => void; onEditResubmit: (messageId: string, newContent: string) => void; onCycleBranch: (rootMessageId: string, direction: -1 | 1) => void; }; const MarkdownBlock = ({ children }: { children: string }) => (
{children}
); export const AgentTurn = React.memo( ({ message, branchState, messageSpeechState, onSpeak, onPause, onResume, onStopSpeech, isTtsSupported, onRegenerate, onEditResubmit, onCycleBranch, }: AgentTurnProps) => { const theme = useTheme(); const isUser = message.role === "user"; const isErrorMessage = Boolean(message.isError); const [isHovered, setIsHovered] = React.useState(false); const [isEditing, setIsEditing] = React.useState(false); const [editDraft, setEditDraft] = React.useState(message.content); const rootMessageId = message.branchRootId ?? message.id; 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)} > {isEditing ? ( setEditDraft(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); if (editDraft.trim() !== message.content) { onEditResubmit(message.id, editDraft); } setIsEditing(false); } else if (e.key === "Escape") { setEditDraft(message.content); setIsEditing(false); } }} sx={{ width: "100%", minHeight: 60, bgcolor: "transparent", border: "none", outline: "none", resize: "none", fontFamily: "inherit", fontSize: "1rem", color: "text.primary", lineHeight: 1.6, }} /> { setEditDraft(message.content); setIsEditing(false); }} sx={{ bgcolor: alpha("#000", 0.05), color: "text.secondary", width: 34, height: 34, "&:hover": { bgcolor: alpha("#000", 0.1) } }} > { onEditResubmit(message.id, editDraft); setIsEditing(false); }} sx={{ bgcolor: editDraft.trim() !== "" && editDraft.trim() !== message.content ? "#00acc1" : alpha("#000", 0.1), color: editDraft.trim() !== "" && editDraft.trim() !== message.content ? "#fff" : "action.disabled", width: 34, height: 34, boxShadow: editDraft.trim() !== "" && editDraft.trim() !== message.content ? `0 4px 12px ${alpha("#00acc1", 0.4)}` : "none", "&:hover": { bgcolor: editDraft.trim() !== "" && editDraft.trim() !== message.content ? "#00838f" : alpha("#000", 0.1) } }} > ) : ( <> {message.content} {isHovered && !isEditing && ( { setIsEditing(true); setEditDraft(message.content); }} aria-label="编辑提问" sx={{ width: 26, height: 26, bgcolor: alpha("#fff", 0.9), color: "#00acc1", boxShadow: `0 2px 8px ${alpha("#000", 0.15)}`, "&:hover": { bgcolor: "#fff", color: "#00838f" } }} > )} {branchState && branchState.total > 1 ? ( onCycleBranch(rootMessageId, -1)} sx={{ width: 22, height: 22, color: "text.secondary", "&:hover": { bgcolor: alpha("#000", 0.08) } }} > {branchState.activeIndex + 1} / {branchState.total} onCycleBranch(rootMessageId, 1)} sx={{ width: 22, height: 22, color: "text.secondary", "&:hover": { bgcolor: alpha("#000", 0.08) } }} > ) : null} )} ); } return ( setIsHovered(true)} onMouseLeave={() => setIsHovered(false)} > TJWater Agent {message.progress?.length ? ( ) : 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 && ( { navigator.clipboard.writeText(message.content); // Could add a toast here }} sx={{ width: 28, height: 28, color: "text.secondary", "&:hover": { color: "#00acc1", bgcolor: alpha("#00acc1", 0.1) } }} > { onRegenerate(); }} sx={{ width: 28, height: 28, color: "text.secondary", "&:hover": { color: "#00acc1", bgcolor: alpha("#00acc1", 0.1) } }} > )} {(!isErrorMessage && isTtsSupported) || (branchState && branchState.total > 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} {branchState && branchState.total > 1 ? ( onCycleBranch(rootMessageId, -1)} sx={{ width: 22, height: 22, color: "text.secondary", "&:hover": { bgcolor: alpha("#000", 0.08) } }} > {branchState.activeIndex + 1} / {branchState.total} onCycleBranch(rootMessageId, 1)} sx={{ width: 22, height: 22, color: "text.secondary", "&:hover": { bgcolor: alpha("#000", 0.08) } }} > ) : null} ) : null} ); }, ); AgentTurn.displayName = "AgentTurn";