feat(agent): render local interactive answers and fold completed process

This commit is contained in:
2026-10-12 18:54:18 +08:00
parent e0eb6a721e
commit 2c5bf15cce
30 changed files with 1610 additions and 75 deletions
@@ -1,3 +1,7 @@
import type { AnswerUiMapTarget } from "../answer-ui/answer-ui-view";
import { AnswerPartsView } from "../answer-ui/answer-parts-view";
import { getAnswerPresentation } from "../answer-ui/answer-presentation";
import { Disclosure } from "@/shared/ui/disclosure";
import {
Activity,
CheckCircle2,
@@ -68,9 +72,7 @@ import {
} from "./agent-motion";
import { AgentHistoryPanel } from "./agent-history-panel";
import { AgentMessageDetails, AgentMessageProgress } from "./agent-message-details";
import { AgentOperationalBrief } from "./agent-operational-brief";
import { AgentUiEnvelopeRenderer } from "./agent-ui-envelope-renderer";
import { StreamingTokenResponse } from "./streaming-token-response";
import { useAgentSpeechRecognition, useAgentSpeechSynthesis } from "../hooks/use-agent-voice";
import {
findSpeechSelectionStartOffset,
@@ -130,6 +132,7 @@ type AgentCommandPanelProps = {
onSelectModel?: (model: string) => void;
onApprovalModeChange?: (mode: AgentApprovalMode) => void;
onSubmitPrompt: (prompt: string) => Promise<void> | void;
onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise<unknown> | void;
onStopPrompt?: () => void;
onReplyPermission?: (
permission: AgentPermissionRequest,
@@ -164,7 +167,6 @@ export function AgentCommandPanel({
sessionHistoryLoading = false,
activeSessionId,
qaOnly = false,
statusLabel = "Agent 后端连接中",
connectionStatus = "connecting",
streaming = false,
messages = [],
@@ -185,6 +187,7 @@ export function AgentCommandPanel({
onSelectModel,
onApprovalModeChange,
onSubmitPrompt,
onLocateAnswerFeatures,
onStopPrompt,
onReplyPermission,
onReplyQuestion,
@@ -327,15 +330,6 @@ export function AgentCommandPanel({
onDeleteSession={onDeleteHistorySession}
/>
);
const operationalBrief = (
<AgentOperationalBrief
messages={messages}
statusLabel={statusLabel}
streaming={streaming}
onSubmitCommand={submitPrompt}
/>
);
return (
<MotionConfig reducedMotion="user">
<aside
@@ -468,45 +462,9 @@ export function AgentCommandPanel({
</header>
</div>
{floatingPresentation ? (
<AnimatePresence initial={false}>
{hasConversation && !historyOpen ? (
<motion.div
key="operational-brief"
className="agent-panel-operational-float absolute z-20 overflow-hidden rounded-2xl"
initial="initial"
animate="animate"
exit="exit"
variants={agentPanelSectionVariants}
>
{operationalBrief}
</motion.div>
) : null}
</AnimatePresence>
) : (
<AnimatePresence initial={false}>
{hasConversation ? (
<motion.div
key="operational-brief"
aria-hidden={historyOpen}
className={cn(
"space-y-2 px-3 pb-3",
historyOpen && "invisible pointer-events-none"
)}
initial="initial"
animate="animate"
exit="exit"
variants={agentPanelSectionVariants}
style={{ overflow: "hidden" }}
>
{operationalBrief}
</motion.div>
) : null}
</AnimatePresence>
)}
<AgentContent className="flex min-h-0 flex-1 flex-col gap-0 p-0">
<AgentContent className="agent-panel-main-content flex min-h-0 flex-1 flex-col gap-0 p-0">
<Conversation
data-agent-conversation-state={hasConversation ? "active" : "empty"}
className={cn(
"agent-panel-conversation min-h-0",
floatingPresentation && "agent-panel-conversation-canvas"
@@ -517,9 +475,6 @@ export function AgentCommandPanel({
"gap-4 p-3",
messages.length === 0 && "min-h-full",
floatingPresentation && "agent-panel-conversation-content",
floatingPresentation &&
hasConversation &&
"agent-panel-conversation-content-has-brief",
floatingPresentation &&
!hasConversation &&
"agent-panel-conversation-content-empty"
@@ -544,6 +499,8 @@ export function AgentCommandPanel({
onResumeSpeech={resumeSpeech}
onStopSpeech={stopSpeech}
qaOnly={qaOnly}
onLocateAnswerFeatures={onLocateAnswerFeatures}
onSubmitAnswerForm={onSubmitPrompt}
/>
) : (
<motion.div
@@ -1233,7 +1190,9 @@ function BackendMessageList({
onPauseSpeech,
onResumeSpeech,
onStopSpeech,
qaOnly
qaOnly,
onLocateAnswerFeatures,
onSubmitAnswerForm
}: {
presentation: AgentCommandPanelPresentation;
messages: AgentChatMessage[];
@@ -1253,6 +1212,8 @@ function BackendMessageList({
onResumeSpeech: () => void;
onStopSpeech: () => void;
qaOnly: boolean;
onLocateAnswerFeatures?: (target: AnswerUiMapTarget) => Promise<unknown> | void;
onSubmitAnswerForm: (text: string) => Promise<void> | void;
}) {
return (
<motion.div
@@ -1261,7 +1222,9 @@ function BackendMessageList({
animate="animate"
>
<AnimatePresence initial={false} mode="popLayout">
{messages.map((message, index) => (
{messages.map((message, index) => {
const answer = getAnswerPresentation(message);
return (
<motion.div
key={message.id}
className={cn("w-full", message.role === "user" && "flex justify-end")}
@@ -1287,7 +1250,7 @@ function BackendMessageList({
依据:{message.taskContext.execution_generation == null ? "历史资料 · 轮次未记录" : `第 ${message.taskContext.execution_generation} 轮`}
{message.taskContext.report_revision == null ? " · 尚无报告" : ` · 报告第 ${message.taskContext.report_revision} 版`}
</p> : null}
{message.role === "assistant" && !qaOnly ? (
{message.role === "assistant" && !qaOnly && !answer.final ? (
<AgentMessageProgress
progress={message.progress}
running={streaming && index === messages.length - 1}
@@ -1307,12 +1270,14 @@ function BackendMessageList({
onResume={onResumeSpeech}
onStop={onStopSpeech}
>
<StreamingTokenResponse
className="leading-6"
content={message.content}
<AnswerPartsView
parts={answer.report}
messageId={message.id}
streamDone={streamRenderState[message.id]?.done ?? !streaming}
streaming={streaming && index === messages.length - 1}
disabled={streaming}
streaming={!answer.final && streaming && index === messages.length - 1}
streamDone={answer.final || (streamRenderState[message.id]?.done ?? !streaming)}
onLocate={onLocateAnswerFeatures}
onSubmit={onSubmitAnswerForm}
/>
</AgentSpeechMessage>
) : (
@@ -1321,7 +1286,16 @@ function BackendMessageList({
) : streaming && index === messages.length - 1 ? (
<span className="text-[var(--ink-muted)]">正在生成...</span>
) : null}
{message.role === "assistant" && !qaOnly ? (
{message.role === "assistant" && answer.final && (answer.process.length > 0 || (!qaOnly && message.progress?.length)) ? (
<Disclosure key="completed-process" label="查看处理过程" className="border-t border-[var(--surface-divider)]">
<div className="space-y-3 py-2">
{!qaOnly ? <AgentMessageProgress progress={message.progress} running={false} /> : null}
<AnswerPartsView parts={answer.process} messageId={`${message.id}-process`} disabled streaming={false} streamDone />
{!qaOnly ? <AgentMessageDetails message={message} /> : null}
</div>
</Disclosure>
) : null}
{message.role === "assistant" && !qaOnly && !answer.final ? (
<AgentMessageDetails
message={message}
onReplyPermission={onReplyPermission}
@@ -1333,7 +1307,8 @@ function BackendMessageList({
</MessageContent>
</Message>
</motion.div>
))}
);
})}
</AnimatePresence>
</motion.div>
);