"use client"; import Image from "next/image"; import React from "react"; import { AnimatePresence, motion } from "framer-motion"; import { Box, Paper, Skeleton, Stack, Typography, alpha, useTheme, Grid } from "@mui/material"; import WaterDropRounded from "@mui/icons-material/WaterDropRounded"; import SensorsRounded from "@mui/icons-material/SensorsRounded"; import TroubleshootRounded from "@mui/icons-material/TroubleshootRounded"; import MapRounded from "@mui/icons-material/MapRounded"; import { AgentTurn } from "./AgentTurn"; import { TypingIndicator } from "./GlobalChatbox.parts"; import type { PermissionReply } from "@/lib/chatStream"; import type { Message, SpeechState, } from "./GlobalChatbox.types"; type AgentWorkspaceProps = { messages: Message[]; isStreaming: boolean; isLoadingSession?: boolean; scrollContainerRef?: React.RefObject; bottomRef: React.RefObject; onScrollStateChange?: (isNearBottom: boolean) => void; speakingMessageId: string | null; speechState: SpeechState; onSpeak: (messageId: string, text: string) => void; onPauseSpeech: () => void; onResumeSpeech: () => 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; }; type TurnListProps = { messages: Message[]; isStreaming: boolean; speakingMessageId: string | null; speechState: SpeechState; onSpeak: (messageId: string, text: string) => void; onPauseSpeech: () => void; onResumeSpeech: () => 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; }; const STREAMING_BOTTOM_RESERVE_PX = 180; const STREAMING_NEAR_BOTTOM_THRESHOLD_PX = STREAMING_BOTTOM_RESERVE_PX + 120; const sameMessages = (left: Message[], right: Message[]) => left.length === right.length && left.every((message, index) => message === right[index]); const TurnListInner = ({ messages, isStreaming, speakingMessageId, speechState, onSpeak, onPauseSpeech, onResumeSpeech, onStopSpeech, isTtsSupported, onCreateBranch, onReplyPermission, onReplyQuestion, onRejectQuestion, }: TurnListProps) => { return ( <> {messages.map((message) => ( ))} ); }; const TurnList = React.memo( TurnListInner, (prevProps, nextProps) => sameMessages(prevProps.messages, nextProps.messages) && prevProps.isStreaming === nextProps.isStreaming && prevProps.speakingMessageId === nextProps.speakingMessageId && prevProps.speechState === nextProps.speechState && prevProps.onSpeak === nextProps.onSpeak && prevProps.onPauseSpeech === nextProps.onPauseSpeech && prevProps.onResumeSpeech === nextProps.onResumeSpeech && prevProps.onStopSpeech === nextProps.onStopSpeech && prevProps.isTtsSupported === nextProps.isTtsSupported && prevProps.onCreateBranch === nextProps.onCreateBranch && prevProps.onReplyPermission === nextProps.onReplyPermission && prevProps.onReplyQuestion === nextProps.onReplyQuestion && prevProps.onRejectQuestion === nextProps.onRejectQuestion, ); TurnList.displayName = "TurnList"; const EmptyState = () => { const theme = useTheme(); const capabilities = [ { icon: , label: "水力瓶颈识别" }, { icon: , label: "异常状态预警" }, { icon: , label: "调度与改造建议" }, { icon: , label: "GIS 地图联动" }, ]; return ( TJWater Agent 我已就绪,请描述任务 你可以使用自然语言下达指令,我会自主规划决策执行、并在地图上呈现分析结果。 {capabilities.map((item) => ( {item.icon} {item.label} ))} ); }; const SessionLoadingSkeleton = () => ( {Array.from({ length: 2 }, (_, turnIndex) => ( ))} ); export const AgentWorkspace = ({ messages, isStreaming, isLoadingSession = false, scrollContainerRef, bottomRef, onScrollStateChange, speakingMessageId, speechState, onSpeak, onPauseSpeech, onResumeSpeech, onStopSpeech, isTtsSupported, onCreateBranch, onReplyPermission, onReplyQuestion, onRejectQuestion, }: AgentWorkspaceProps) => { const theme = useTheme(); const latestAssistant = [...messages] .reverse() .find((message) => message.role === "assistant"); const showTypingIndicator = isStreaming && (!latestAssistant || (latestAssistant.content.trim().length === 0 && !(latestAssistant.artifacts?.length))); const streamingMessage = isStreaming && messages.at(-1)?.role === "assistant" ? messages.at(-1) : undefined; const historyMessages = streamingMessage !== undefined ? messages.slice(0, -1) : messages; const handleScroll = React.useCallback( (event: React.UIEvent) => { if (!onScrollStateChange) return; const target = event.currentTarget; const distanceToBottom = target.scrollHeight - target.scrollTop - target.clientHeight; onScrollStateChange( distanceToBottom < (isStreaming ? STREAMING_NEAR_BOTTOM_THRESHOLD_PX : 96), ); }, [isStreaming, onScrollStateChange], ); return ( {isLoadingSession ? ( ) : ( <> {messages.length === 0 ? : null} {messages.length > 0 ? ( {streamingMessage ? ( ) : null} ) : null} )} {!isLoadingSession && showTypingIndicator ? ( ) : null}
); };