"use client";
import React from "react";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import { motion } from "framer-motion";
import {
Avatar,
Box,
Chip,
IconButton,
LinearProgress,
Paper,
Stack,
Typography,
alpha,
} from "@mui/material";
import type { Theme } from "@mui/material/styles";
import AutoAwesome from "@mui/icons-material/AutoAwesome";
import CheckCircleRounded from "@mui/icons-material/CheckCircleRounded";
import ErrorOutlineRounded from "@mui/icons-material/ErrorOutlineRounded";
import HourglassEmptyRounded from "@mui/icons-material/HourglassEmptyRounded";
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 {
parseAssistantMessageSections,
parseContentWithToolCalls,
type ContentSegment,
} from "./chatMessageSections";
import { ChatInlineChart } from "./ChatInlineChart";
import { ChatToolCallBlock } from "./ChatToolCallBlock";
import markdownStyles from "./GlobalChatboxMarkdown.module.css";
import type { ChatProgress, Message, SpeechState } from "./GlobalChatbox.types";
import { stripMarkdown } from "./GlobalChatbox.utils";
export const TypingIndicator = () => {
return (
{[0, 1, 2].map((i) => (
))}
);
};
export const Blob = ({
color,
size,
top,
left,
delay,
}: {
color: string;
size: number;
top: string;
left: string;
delay: number;
}) => (
);
type ChatMessageItemProps = {
message: Message;
theme: Theme;
messageSpeechState: SpeechState;
onSpeak: (messageId: string, text: string) => void;
onPause: () => void;
onResume: () => void;
onStopSpeech: () => void;
isTtsSupported: boolean;
sseChartParams?: Array<{ tool: string; params: Record }>;
};
export const ChatMessageItem = React.memo(
({
message,
theme,
messageSpeechState,
onSpeak,
onPause,
onResume,
onStopSpeech,
isTtsSupported,
sseChartParams,
}: ChatMessageItemProps) => {
const isUser = message.role === "user";
const isErrorMessage = Boolean(message.isError);
const parsedAssistantSections =
!isUser && !isErrorMessage
? parseAssistantMessageSections(message.content)
: null;
const answerContent = parsedAssistantSections?.answer ?? message.content;
const contentSegments: ContentSegment[] =
!isUser && !isErrorMessage
? parseContentWithToolCalls(answerContent).segments
: [{ type: "text", content: answerContent }];
return (
{!isUser && (
{isErrorMessage ? (
) : (
)}
)}
{!isUser && !isErrorMessage && 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") {
return (
)}
/>
);
}
if (segment.toolCall.tool === "show_chart") {
const p = segment.toolCall.params;
return (
);
}
return (
);
}
if (segment.type === "tool_call_pending") {
return (
正在准备工具调用...
);
}
return null;
})}
{sseChartParams?.map((chart, idx) => (
))}
{!isUser && !isErrorMessage && isTtsSupported && (
{messageSpeechState === "idle" && (
onSpeak(message.id, stripMarkdown(answerContent))}
aria-label="朗读消息"
sx={{
color: "text.secondary",
opacity: 0.6,
"&:hover": { opacity: 1 },
p: 0.5,
}}
>
)}
{messageSpeechState === "playing" && (
<>
>
)}
{messageSpeechState === "paused" && (
<>
>
)}
)}
);
},
);
ChatMessageItem.displayName = "ChatMessageItem";
const ChatProgressPanel = ({ progress }: { progress: ChatProgress[] }) => {
const isComplete = progress.some(
(item) => item.phase === "complete" && item.status === "completed",
);
const latestRunning = isComplete
? undefined
: [...progress].reverse().find((item) => item.status === "running");
return (
Agent 过程
{latestRunning ? (
) : null}
{latestRunning ? : null}
{progress.slice(-5).map((item) => (
{item.status === "completed" ? (
) : item.status === "error" ? (
) : (
)}
{item.title}
{item.detail ? (
{item.detail}
) : null}
))}
);
};