import type { ComponentProps, Dispatch, Ref, SetStateAction } from "react"; import type { PersonaState } from "@/shared/ai-elements/persona"; import type { StatusTone } from "@/shared/ui/status"; import type { AgentChatSessionSummary, AgentInputDelivery, AgentResponseInput } from "../api/client"; import type { AgentApprovalMode, AgentChatMessage, AgentModelOption, AgentPermissionReply, AgentPermissionRequest, AgentQuestionRequest, AgentStreamRenderState, AgentUiResult } from "../types"; import type { AgentSpeakOptions, AgentSpeechState } from "../speech"; import type { AnswerUiMapTarget } from "../answer-ui/answer-ui-view"; import type { AnswerUiSelection } from "../answer-ui/answer-ui-interaction-context"; import type { AnswerViewStateScope } from "../session-state/answer-view-state-scope"; import { AgentInteractionReply, AgentInteractionRequests } from "./agent-interaction-request-card"; import { canReplaceInteractionRequest } from "../api/interaction-requests"; import type { InteractionRequestsController } from "../hooks/use-interaction-requests"; import { AnswerViewStateProvider } from "../session-state/answer-view-state-context"; import { AnswerPartsView } from "../answer-ui/answer-parts-view"; import { ExpandableReading } from "@/shared/ui/reading-workspace"; import { canExpandAnswer, getAnswerPartsTranscript, getAnswerPresentation } from "../answer-ui/answer-presentation"; import { AgentProcessDisclosure } from "./agent-process-disclosure"; import { AgentMessageProcessContent } from "./agent-message-process-content"; import { groupMessageProcesses, hasPendingMessageInput } from "../presentation/message-process-groups"; import { AgentUserMessageText } from "./agent-user-message-text"; import { useAgentAnswerTransition } from "../hooks/use-agent-answer-transition"; import { copySelectedText } from "@/shared/ai-elements/paragraph-copy"; import { ArrowUp, ChevronLeft, Eye, History, Plus, Quote, Square, X } from "lucide-react"; import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react"; import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import { useStickToBottomContext } from "use-stick-to-bottom"; import { showMapNotice } from "@/features/map/core"; import { Agent, AgentContent } from "@/shared/ai-elements/agent"; import { Conversation, ConversationContent, ConversationEmptyState, ConversationScrollButton } from "@/shared/ai-elements/conversation"; import { Message, MessageContent } from "@/shared/ai-elements/message"; import { PromptInput, PromptInputBody, PromptInputFooter, PromptInputSubmit, PromptInputTextarea } from "@/shared/ai-elements/prompt-input"; import { Suggestion, Suggestions } from "@/shared/ai-elements/suggestion"; import { Button } from "@/shared/ui/button"; import { Separator } from "@/shared/ui/separator"; import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/shared/ui/tooltip"; import { cn } from "@/shared/ui/cn"; import { StatusDot } from "@/shared/ui/status"; import { AgentChatMessageActions, AgentChatMessageHeader, AgentChatMessageToolbar } from "./agent-chat-message-chrome"; import { agentLayoutTransition, agentPanelItemVariants, agentPanelListVariants } from "./agent-motion"; import { AgentPersona } from "./agent-persona"; import { AgentTopicControls } from "./agent-topic-controls"; import { AgentHistoryPanel } from "./agent-history-panel"; import { AgentMessageInbox } from "./agent-message-inbox"; import { AgentPendingInputs } from "./agent-pending-inputs"; import { AgentMessageDetails } from "./agent-message-details"; import { AgentUiEnvelopeRenderer } from "./agent-ui-envelope-renderer"; import { useAgentSpeechRecognition, useAgentSpeechSynthesis } from "../hooks/use-agent-voice"; import { stripSpeechMarkdown } from "../speech"; import type { AgentConnectionStatus } from "../types"; import { VoiceInputButton, InputDeliverySelect, ApprovalModeSelect, AgentModelSelect } from "./agent-panel-controls"; import { AgentSpeechMessage, AgentMessageSpeechControls } from "./agent-message-speech"; export type AgentCommandPanelPresentation = "desktop-dock" | "desktop-floating" | "mobile-sheet"; export type AgentWorkspaceContextItem = { id: string; label: string; value: string; tone?: "neutral" | "info" | "warning"; }; type AgentCommandPanelProps = { presentation?: AgentCommandPanelPresentation; collapsing?: boolean; personaState?: PersonaState; sessionTitle?: string; sessionHistory?: AgentChatSessionSummary[]; sessionHistoryLoading?: boolean; historyQuery?: string; historyHasMore?: boolean; historyLoadingMore?: boolean; historyError?: string; historyPendingUpdate?: boolean; onHistoryQueryChange?: (value: string) => void; onHistoryOpen?: () => Promise | void; onHistoryClose?: () => void; onLoadMoreHistory?: () => Promise | void; hasOlderMessages?: boolean; loadingOlderMessages?: boolean; onLoadOlderMessages?: () => Promise | void; backgroundStatus?: string; readingPosition?: { messageId?: string; offset: number; atBottom: boolean }; onReadingPositionChange?: (position: { messageId?: string; offset: number; atBottom: boolean }) => void; promptSuggestions?: Array<{ question: string; sourceLabel?: string }>; promptSuggestionsLoading?: boolean; promptSuggestionsUnavailable?: boolean; onRetryPromptSuggestions?: () => void; activeSessionId?: string | null; answerStateScope?: AnswerViewStateScope; qaOnly?: boolean; executionHistory?: boolean; statusLabel?: string; connectionStatus?: AgentConnectionStatus; streaming?: boolean; inputDelivery?: AgentInputDelivery; pendingInputs?: AgentResponseInput[]; onInputDeliveryChange?: (delivery: AgentInputDelivery) => void; onPendingInputDeliveryChange?: (id: string, delivery: AgentInputDelivery) => Promise | void; onPendingInputEdit?: (id: string) => Promise | void; onPendingInputCancel?: (id: string) => Promise | void; onDownloadFile?: (path: string) => Promise | void; messages?: AgentChatMessage[]; streamRenderState?: AgentStreamRenderState; modelOptions?: AgentModelOption[]; selectedModel?: string; currentProjectName?: string | null; topicControls?: ComponentProps; historyTopicId?: string; onHistoryTopicChange?: (id: string | undefined) => void; onMoveSessionToTopic?: (sessionId: string, topicId: string | null) => Promise; approvalMode?: AgentApprovalMode; uiResults?: AgentUiResult[]; interactionRequests?: InteractionRequestsController; messageInbox?: ComponentProps; workspaceContext?: AgentWorkspaceContextItem[]; prompt: string; promptFocusRequest?: number; promptReference?: { title: string; detail?: string; placeholder?: string }; contentReference?: AnswerUiSelection; onContentReferenceChange?: (selection: AnswerUiSelection | undefined) => void; onCollapse: () => void; onPromptChange: Dispatch>; onRefreshHistory?: () => Promise | void; onStartNewSession?: () => Promise | void; onLoadHistorySession?: (sessionId: string) => Promise | void; onRenameHistorySession?: (sessionId: string, title: string) => Promise | void; onDeleteHistorySession?: (sessionId: string) => Promise | void; onSelectModel?: (model: string) => void; onApprovalModeChange?: (mode: AgentApprovalMode) => void; onSubmitPrompt: (prompt: string) => Promise | void; onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise | void; onStopPrompt?: () => void; onReplyPermission?: ( permission: AgentPermissionRequest, reply: AgentPermissionReply ) => Promise | void; onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise | void; onRejectQuestion?: (request: AgentQuestionRequest) => Promise | void; }; export function AgentCommandPanel({ presentation = "desktop-dock", collapsing = false, personaState, sessionTitle = "新对话", sessionHistory = [], sessionHistoryLoading = false, historyQuery, historyHasMore, historyLoadingMore, historyError, historyPendingUpdate, onHistoryQueryChange, onHistoryOpen, onHistoryClose, onLoadMoreHistory, hasOlderMessages, loadingOlderMessages, onLoadOlderMessages, backgroundStatus, readingPosition, onReadingPositionChange, promptSuggestions = [], promptSuggestionsLoading = false, promptSuggestionsUnavailable = false, onRetryPromptSuggestions, activeSessionId, answerStateScope, qaOnly = false, executionHistory = false, connectionStatus = "connecting", streaming = false, inputDelivery = "steer", pendingInputs = [], onInputDeliveryChange, onPendingInputDeliveryChange, onPendingInputEdit, onPendingInputCancel, onDownloadFile, messages = [], streamRenderState = {}, modelOptions = [], selectedModel = "", currentProjectName, topicControls, historyTopicId, onHistoryTopicChange, onMoveSessionToTopic, approvalMode = "request", uiResults = [], interactionRequests, messageInbox, prompt, promptFocusRequest = 0, promptReference, contentReference, onContentReferenceChange, onCollapse, onPromptChange, onRefreshHistory, onStartNewSession, onLoadHistorySession, onRenameHistorySession, onDeleteHistorySession, onSelectModel, onApprovalModeChange, onSubmitPrompt, onLocateAnswerFeatures, onStopPrompt, onReplyPermission, onReplyQuestion, onRejectQuestion }: AgentCommandPanelProps) { const [historyOpen, setHistoryOpen] = useState(false); const [historySelectionError, setHistorySelectionError] = useState(); const [loadingSessionId, setLoadingSessionId] = useState(null); const [scrollRequestId, setScrollRequestId] = useState(0); const [submissionError, setSubmissionError] = useState(null); const prefersReducedMotion = useReducedMotion(); const historyTriggerRef = useRef(null); const promptRestoreGenerationRef = useRef(0); const promptTextareaRef = useRef(null); const mountedRef = useRef(true); const desktopFloating = presentation === "desktop-floating"; const floatingPresentation = desktopFloating; const hasConversation = messages.length > 0 || streaming || Boolean(messageInbox?.messages.length); // Task discussions keep their reference and composer layout before the first message. const centeredComposer = !hasConversation && !loadingSessionId && !promptReference && uiResults.length === 0 && !messageInbox?.loading && !messageInbox?.error; const showStartSuggestions = centeredComposer && !qaOnly && !promptReference; const trimmedPrompt = prompt.trim(); const isStopAction = streaming && !trimmedPrompt; const acceptsAdditionalInput = streaming || Boolean(interactionRequests?.awaitingInput); useEffect(() => { if (!promptFocusRequest) return; const frame = requestAnimationFrame(() => { const textarea = promptTextareaRef.current; textarea?.focus(); textarea?.setSelectionRange(textarea.value.length, textarea.value.length); }); return () => cancelAnimationFrame(frame); }, [promptFocusRequest]); const { speechState, speakingMessageId, speak, pause: pauseSpeech, resume: resumeSpeech, stop: stopSpeech, isSupported: isSpeechSupported } = useAgentSpeechSynthesis((message) => { showMapNotice({ id: "agent-speech-playback-error", tone: "error", title: "语音播放失败", message, duration: 5000 }); }); const appendRecognitionResult = useCallback((text: string) => { promptRestoreGenerationRef.current += 1; onPromptChange((current) => `${current}${current.trim() ? " " : ""}${text}`); }, [onPromptChange]); const showRecognitionError = useCallback((message: string) => { showMapNotice({ id: "agent-speech-recognition-error", tone: "error", title: "语音输入不可用", message, duration: 5000 }); }, []); const { isListening, start: startListening, stop: stopListening, isSupported: isRecognitionSupported } = useAgentSpeechRecognition(appendRecognitionResult); useEffect(() => { mountedRef.current = true; return () => { mountedRef.current = false; promptRestoreGenerationRef.current += 1; }; }, []); useEffect(() => { stopSpeech(); setSubmissionError(null); }, [activeSessionId, stopSpeech]); useEffect(() => { if (streaming) stopListening(); }, [stopListening, streaming]); const submitPrompt = (nextPrompt: string) => { if (!nextPrompt) { return; } const submittedReference = contentReference; const message = submittedReference ? `${nextPrompt}\n\n引用内容:${submittedReference.title}\n${submittedReference.text}` : nextPrompt; if (message.length > 10000) { setSubmissionError(`消息与引用合计 ${message.length.toLocaleString()} 字,超过 10,000 字。请缩短问题或减少引用内容后再发送。`); return; } setSubmissionError(null); const restoreGeneration = promptRestoreGenerationRef.current + 1; promptRestoreGenerationRef.current = restoreGeneration; onPromptChange(""); stopListening(); setScrollRequestId((requestId) => requestId + 1); onContentReferenceChange?.(undefined); void Promise.resolve().then(() => onSubmitPrompt(message)).catch(() => { if (!mountedRef.current || promptRestoreGenerationRef.current !== restoreGeneration) { return; } onPromptChange((current) => current || nextPrompt); if (submittedReference) onContentReferenceChange?.(submittedReference); }); }; const historyPanel = ( { setHistoryOpen(open); if (!open) onHistoryClose?.(); }} triggerRef={historyTriggerRef} scopeLabel={currentProjectName ?? "当前项目"} query={historyQuery} onQueryChange={onHistoryQueryChange} hasMore={historyHasMore} loadingMore={historyLoadingMore} error={historySelectionError ?? historyError} pendingUpdate={historyPendingUpdate} onLoadMore={onLoadMoreHistory} activeSessionId={activeSessionId} loadingSessionId={loadingSessionId} loading={sessionHistoryLoading} sessions={sessionHistory} topics={topicControls?.topics} topicId={historyTopicId} onTopicChange={onHistoryTopicChange} onMoveSessionToTopic={onMoveSessionToTopic} onRefresh={onRefreshHistory} onSelectSession={(nextSessionId) => { promptRestoreGenerationRef.current += 1; setLoadingSessionId(nextSessionId); setHistorySelectionError(undefined); void Promise.resolve(onLoadHistorySession?.(nextSessionId)) .then(() => { setHistoryOpen(false); onHistoryClose?.(); }) .catch((error: unknown) => setHistorySelectionError(error instanceof Error ? error.message : "无法打开这个历史会话")) .finally(() => setLoadingSessionId(null)); }} onRenameSession={onRenameHistorySession} onDeleteSession={onDeleteHistorySession} /> ); return ( {historyPanel} { const target = event.target instanceof Element ? event.target.closest("a") : null; const href = target?.getAttribute("href"); if (!href?.startsWith("/api/v1/session/") || !href.includes("/files/")) return; event.preventDefault(); event.stopPropagation(); void onDownloadFile?.(href); }} className={cn( "pointer-events-auto flex h-full min-h-0 w-full flex-col", "agent-panel-shell", presentation === "desktop-dock" ? "surface-dock overflow-hidden rounded-none border-0 border-r" : desktopFloating ? "acrylic-panel overflow-visible rounded-2xl border" : "overflow-hidden rounded-none border-0 bg-transparent", collapsing && "pointer-events-none" )} >

供水助手

{agentConnectionMetadata[connectionStatus].label}

{topicControls && !qaOnly ? : null}
{presentation === "mobile-sheet" ? ( ) : ( )}
{hasConversation || sessionTitle !== "新对话" ?

{sessionTitle}

: null} {backgroundStatus ? : null} {loadingSessionId && !hasConversation ? (

正在加载对话…

) : null} {messages.length > 0 || Boolean(messageInbox?.messages.length) ? ( ) : null} {!qaOnly ? : null} {interactionRequests ? message.role === "user").map((message) => message.id))} /> : null} {messageInbox ? : null}
{executionHistory ? (

执行记录自动同步

发送消息后进入独立调试会话,系统执行记录不会被修改。

) : null} {centeredComposer && !promptReference ? (

有什么可以帮你?

) : null} { const nextPrompt = message.text.trim(); if (nextPrompt) { submitPrompt(nextPrompt); } }} > {contentReference ?
{contentReference.title} 查看引用内容

{contentReference.text}

{["解释这些内容", "比较差异与关联", "核查依据与缺口"].map((intent) => )}
: null} {promptReference ? (
) : null} { setSubmissionError(null); promptRestoreGenerationRef.current += 1; onPromptChange(event.currentTarget.value); }} className={cn("agent-panel-prompt-textarea text-base", floatingPresentation ? "min-h-20" : "min-h-14")} aria-label={promptReference?.placeholder ?? "询问供水问题,或描述需要处理的任务"} placeholder={promptReference?.placeholder ?? (streaming ? "继续补充,或安排下一条消息…" : "询问供水问题,或描述需要处理的任务")} />
{submissionError ?

{submissionError}

: null}
{acceptsAdditionalInput ? : } {isRecognitionSupported && !streaming ? ( startListening(showRecognitionError) } /> ) : null} {isStopAction ? :
{showStartSuggestions ? ( {promptSuggestionsLoading && promptSuggestions.length === 0 ? (

正在根据最近活动整理问题…

) : promptSuggestions.length ? promptSuggestions.map((suggestion) => ( {suggestion.question} {suggestion.sourceLabel ? 来源:{suggestion.sourceLabel} : null}

{suggestion.question}

{suggestion.sourceLabel ?

{suggestion.sourceLabel}

: null}
)) : (

{promptSuggestionsUnavailable ? "推荐问题暂不可用,可直接输入任务。" : "描述调查目标,即可开始分析。"}

{promptSuggestionsUnavailable && onRetryPromptSuggestions && ( )}
)}
) : null}
); } type AgentConversationScrollSnapshot = { activeSessionId?: string | null; lastMessageRevision: string; lastUserMessageId: string | null; scrollRequestId: number; streaming: boolean; }; function AgentConversationScrollManager({ activeSessionId, messages, scrollRequestId, streaming, readingPosition, onPositionChange, }: { activeSessionId?: string | null; messages: AgentChatMessage[]; scrollRequestId: number; streaming: boolean; readingPosition?: { messageId?: string; offset: number; atBottom: boolean }; onPositionChange?: (position: { messageId?: string; offset: number; atBottom: boolean }) => void; }) { const { scrollToBottom, scrollRef, stopScroll } = useStickToBottomContext(); const positionRef = useRef(readingPosition); const onPositionRef = useRef(onPositionChange); positionRef.current = readingPosition; onPositionRef.current = onPositionChange; useEffect(() => { const scroll = scrollRef.current; if (!scroll) return; const save = () => { const top = scroll.getBoundingClientRect().top; const message = [...scroll.querySelectorAll("[data-agent-message-id]")] .find((element) => element.getBoundingClientRect().bottom > top); onPositionRef.current?.({ messageId: message?.dataset.agentMessageId, offset: message ? message.getBoundingClientRect().top - top : 0, atBottom: scroll.scrollHeight - scroll.clientHeight - scroll.scrollTop < 24 }); }; scroll.addEventListener("scroll", save, { passive: true }); return () => scroll.removeEventListener("scroll", save); }, [activeSessionId, scrollRef]); const previousSnapshotRef = useRef(null); const lastUserMessageId = getLastUserMessageId(messages); const lastMessageRevision = getLastMessageRevision(messages); const snapshot = useMemo( () => ({ activeSessionId, lastMessageRevision, lastUserMessageId, scrollRequestId, streaming }), [activeSessionId, lastMessageRevision, lastUserMessageId, scrollRequestId, streaming] ); useEffect(() => { const previousSnapshot = previousSnapshotRef.current; previousSnapshotRef.current = snapshot; const forceScroll = !previousSnapshot || previousSnapshot.activeSessionId !== snapshot.activeSessionId || previousSnapshot.scrollRequestId !== snapshot.scrollRequestId; if (!forceScroll) { return; } const animationFrameId = window.requestAnimationFrame(() => { const position = positionRef.current; if (previousSnapshot?.activeSessionId !== snapshot.activeSessionId && position && !position.atBottom && position.messageId) { const scroll = scrollRef.current; const target = scroll?.querySelector(`[data-agent-message-id="${CSS.escape(position.messageId)}"]`); if (scroll && target) { stopScroll(); scroll.scrollTop += target.getBoundingClientRect().top - scroll.getBoundingClientRect().top - position.offset; return; } } void scrollToBottom({ animation: "instant", ignoreEscapes: true, wait: false }); }); return () => window.cancelAnimationFrame(animationFrameId); }, [scrollRef, scrollToBottom, snapshot, stopScroll]); return null; } function AgentOlderMessages({ hasMore, loading, onLoad, sessionId, messages }: { hasMore?: boolean; loading?: boolean; onLoad?: () => Promise | void; sessionId?: string | null; messages: AgentChatMessage[]; }) { const { scrollRef, stopScroll } = useStickToBottomContext(); const anchor = useRef<{ id?: string; top: number; height: number; sessionId?: string | null } | null>(null); const pending = useRef(false); const load = useCallback(async () => { const scroll = scrollRef.current; if (!scroll || !hasMore || loading || pending.current || !onLoad) return; const visible = [...scroll.querySelectorAll("[data-agent-message-id]")] .find((element) => element.getBoundingClientRect().bottom > scroll.getBoundingClientRect().top); anchor.current = { id: visible?.dataset.agentMessageId, top: visible?.getBoundingClientRect().top ?? 0, height: scroll.scrollHeight, sessionId }; pending.current = true; stopScroll(); try { await onLoad(); } finally { pending.current = false; } }, [hasMore, loading, onLoad, scrollRef, sessionId, stopScroll]); useLayoutEffect(() => { const prior = anchor.current; const scroll = scrollRef.current; if (!prior || !scroll || prior.sessionId !== sessionId || scroll.scrollHeight === prior.height) return; const visible = prior.id ? scroll.querySelector(`[data-agent-message-id="${CSS.escape(prior.id)}"]`) : null; if (visible) scroll.scrollTop += visible.getBoundingClientRect().top - prior.top; anchor.current = null; }, [messages, scrollRef, sessionId]); useEffect(() => { const scroll = scrollRef.current; if (!scroll) return; const onScroll = () => { if (scroll.scrollTop < 64) void load(); }; scroll.addEventListener("scroll", onScroll, { passive: true }); return () => scroll.removeEventListener("scroll", onScroll); }, [load, scrollRef]); return hasMore ? : null; } function getLastMessageRevision(messages: AgentChatMessage[]) { const lastMessage = messages.at(-1); if (!lastMessage) { return ""; } const progressRevision = lastMessage.progress?.map((item) => `${item.id}:${item.status}`).join("|") ?? ""; return `${lastMessage.id}:${lastMessage.content.length}:${progressRevision}`; } function getLastUserMessageId(messages: AgentChatMessage[]) { for (let index = messages.length - 1; index >= 0; index -= 1) { if (messages[index].role === "user") { return messages[index].id; } } return null; } const agentConnectionMetadata = { connecting: { label: "连接中", tone: "info" }, online: { label: "在线", tone: "success" }, offline: { label: "离线", tone: "danger" } } satisfies Record; type BackendMessageListProps = { presentation: AgentCommandPanelPresentation; messages: AgentChatMessage[]; streaming: boolean; streamRenderState: AgentStreamRenderState; onReplyPermission?: (permission: AgentPermissionRequest, reply: AgentPermissionReply) => Promise | void; onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise | void; onRejectQuestion?: (request: AgentQuestionRequest) => Promise | void; isSpeechSupported: boolean; speakingMessageId: string | null; speechState: AgentSpeechState; onSpeak: (messageId: string, text: string, options?: AgentSpeakOptions) => Promise | void; onPauseSpeech: () => void; onResumeSpeech: () => void; onStopSpeech: () => void; qaOnly: boolean; showProcess: boolean; onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise | void; onSubmitAnswerForm: (text: string) => Promise | void; interactionRequests?: InteractionRequestsController; }; function BackendMessageList(props: BackendMessageListProps) { const { messages, streaming, interactionRequests } = props; const replies = new Map(); for (const message of messages) { if (message.role === "user" && message.interactionReply) replies.set(message.id, { record: message.interactionReply, draft: message.interactionReply.draft, dirty: false, busy: false, }); } for (const entry of interactionRequests?.entries ?? []) { if (!entry.record.delivery || !["answered", "skipped"].includes(entry.record.status)) continue; const id = entry.record.delivery.native_message_id; const previous = replies.get(id); if (!previous || canReplaceInteractionRequest(entry.record, previous.record)) replies.set(id, entry); } const processGroups = groupMessageProcesses(messages, [...replies.values()].map(({ record }) => record)); return ( {messages.map((message, index) => { return ( ); })} ); } function BackendMessage({ ref, message: incomingMessage, processMessages: incomingProcessMessages, running, reply, presentation, streamRenderState, onReplyPermission, onReplyQuestion, onRejectQuestion, isSpeechSupported, speakingMessageId, speechState, onSpeak, onPauseSpeech, onResumeSpeech, onStopSpeech, qaOnly, showProcess, onLocateAnswerFeatures, onSubmitAnswerForm, interactionRequests, }: BackendMessageListProps & { ref?: Ref; message: AgentChatMessage; processMessages: AgentChatMessage[]; running: boolean; reply?: InteractionRequestsController["entries"][number]; }) { const transition = useAgentAnswerTransition(incomingMessage, running && incomingMessage.role === "assistant"); const { message, completed } = transition; const processMessages = incomingProcessMessages.map((source) => (source.id === message.id ? message : source)); const answer = getAnswerPresentation(message); const needsReply = hasPendingMessageInput(message); if (message.role === "assistant" && !processMessages.length && !answer.report.length && !needsReply) return null; const reportText = getAnswerPartsTranscript(answer.report); const hasOutput = answer.report.length > 0; const speech = completed && hasOutput && isSpeechSupported ? ( void Promise.resolve(onSpeak(message.id, stripSpeechMarkdown(reportText)))} onPause={onPauseSpeech} onResume={onResumeSpeech} onStop={onStopSpeech} /> ) : null; const copy = completed ? : null; const processComplete = completed; const hasProcess = message.role === "assistant" && processMessages.some( (source) => getAnswerPresentation(source).process.length > 0 || (showProcess && source.progress?.length) || (!qaOnly && !hasPendingMessageInput(source) && (source.todos?.todos.length || source.permissions?.length || source.questions?.length)), ); const hasProcessError = processMessages.some((source) => source.progress?.some((item) => item.status === "error")); return ( {message.role === "assistant" ? : null} {message.role === "assistant" && message.taskContext ? (

依据: {message.taskContext.execution_generation == null ? "历史资料 · 轮次未记录" : `第 ${message.taskContext.execution_generation} 轮`} {message.taskContext.report_revision == null ? " · 尚无报告" : ` · 报告第 ${message.taskContext.report_revision} 版`}

) : null} {hasOutput ? ( message.role === "assistant" ? ( {answer.report.find((part) => part.type === "text")?.text ?? "交互内容已在工作区展开。"}

} >
) : reply ? ( void interactionRequests?.act(reply.record.interaction_id, "resume")} /> ) : ( ) ) : null} {message.role === "assistant" && processComplete && hasProcessError ? (

部分查询或工具未完成,可展开处理过程查看原因。

) : null} {message.role === "assistant" && !qaOnly && (needsReply || (!completed && processMessages.length > 0) || (processMessages.length > 0 && !hasProcess)) ? ( ) : null} {hasProcess ? ( ( {trigger} )} > ) : null} {message.role === "assistant" && completed && hasOutput && !hasProcess ? ( ) : null}
); }