"use client"; import { Activity, CheckCircle2, ChevronsUpDown, ChevronLeft, DatabaseZap, GitMerge, History, MapPinned, Plus, SendHorizontal } from "lucide-react"; import { AnimatePresence, MotionConfig, motion } from "motion/react"; import { useEffect, useMemo, useRef, useState } from "react"; import { useStickToBottomContext } from "use-stick-to-bottom"; import { Agent, AgentContent } from "@/shared/ai-elements/agent"; import { Conversation, ConversationContent, ConversationScrollButton } from "@/shared/ai-elements/conversation"; import type { PersonaState } from "@/shared/ai-elements/persona"; 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 { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuTrigger } from "@/shared/ui/dropdown-menu"; import { Button } from "@/shared/ui/button"; import { cn } from "@/lib/utils"; import type { AgentChatSessionSummary } from "../api/client"; import { AgentPersona } from "./agent-persona"; import { agentLayoutTransition, agentPanelItemVariants, agentPanelListVariants, agentPanelSectionVariants } 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 type { AgentChatMessage, AgentModelOption, AgentPermissionReply, AgentPermissionRequest, AgentQuestionRequest, AgentStreamRenderState, AgentUiResult } from "../types"; type AgentCommandPanelProps = { collapsing?: boolean; personaState?: PersonaState; sessionTitle?: string; sessionHistory?: AgentChatSessionSummary[]; sessionHistoryLoading?: boolean; activeSessionId?: string | null; statusLabel?: string; streaming?: boolean; messages?: AgentChatMessage[]; streamRenderState?: AgentStreamRenderState; modelOptions?: AgentModelOption[]; selectedModel?: string; uiResults?: AgentUiResult[]; onCollapse: () => void; 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; onSubmitPrompt: (prompt: string) => Promise | void; onStopPrompt?: () => void; onReplyPermission?: (permission: AgentPermissionRequest, reply: AgentPermissionReply) => Promise | void; onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise | void; onRejectQuestion?: (request: AgentQuestionRequest) => Promise | void; }; const AGENT_PROMPT_SUGGESTIONS = [ "当前监测数据质量如何,是否存在缺失、失真或可信度不足的问题?", "当前监测数据中有哪些异常问题需要重点关注?", "当前管网哪些区域可能存在雨污混接,判断依据是什么?" ] as const; const AGENT_CAPABILITIES = [ { icon: DatabaseZap, label: "监测质量诊断" }, { icon: Activity, label: "异常状态研判" }, { icon: GitMerge, label: "雨污混接识别" }, { icon: MapPinned, label: "GIS 地图联动" } ] as const; export function AgentCommandPanel({ collapsing = false, personaState, sessionTitle = "新对话", sessionHistory = [], sessionHistoryLoading = false, activeSessionId, statusLabel = "Agent 后端连接中", streaming = false, messages = [], streamRenderState = {}, modelOptions = [], selectedModel = "", uiResults = [], onCollapse, onRefreshHistory, onStartNewSession, onLoadHistorySession, onRenameHistorySession, onDeleteHistorySession, onSelectModel, onSubmitPrompt, onStopPrompt, onReplyPermission, onReplyQuestion, onRejectQuestion }: AgentCommandPanelProps) { const [prompt, setPrompt] = useState(""); const [historyOpen, setHistoryOpen] = useState(false); const [loadingSessionId, setLoadingSessionId] = useState(null); const [scrollRequestId, setScrollRequestId] = useState(0); const trimmedPrompt = prompt.trim(); useEffect(() => { if (historyOpen) { void Promise.resolve(onRefreshHistory?.()); } }, [historyOpen, onRefreshHistory]); const submitPrompt = (nextPrompt: string) => { if (!nextPrompt || streaming) { return; } setPrompt(""); setScrollRequestId((requestId) => requestId + 1); void Promise.resolve(onSubmitPrompt(nextPrompt)).catch(() => { setPrompt(nextPrompt); }); }; return ( ); } type AgentConversationScrollSnapshot = { activeSessionId?: string | null; lastUserMessageId: string | null; scrollRequestId: number; signature: string; }; function AgentConversationScrollManager({ activeSessionId, messages, scrollRequestId, streaming, uiResults }: { activeSessionId?: string | null; messages: AgentChatMessage[]; scrollRequestId: number; streaming: boolean; uiResults: AgentUiResult[]; }) { const { escapedFromLock, isAtBottom, scrollToBottom, state } = useStickToBottomContext(); const previousSnapshotRef = useRef(null); const wasPinnedToBottom = !escapedFromLock && (isAtBottom || state.isAtBottom || state.isNearBottom); const snapshot = useMemo( () => ({ activeSessionId, lastUserMessageId: getLastUserMessageId(messages), scrollRequestId, signature: createAgentConversationScrollSignature(messages, streaming, uiResults) }), [activeSessionId, messages, scrollRequestId, streaming, uiResults] ); useEffect(() => { const previousSnapshot = previousSnapshotRef.current; previousSnapshotRef.current = snapshot; const forceScroll = !previousSnapshot || previousSnapshot.activeSessionId !== snapshot.activeSessionId || previousSnapshot.scrollRequestId !== snapshot.scrollRequestId || (Boolean(snapshot.lastUserMessageId) && previousSnapshot.lastUserMessageId !== snapshot.lastUserMessageId); const contentChanged = !previousSnapshot || previousSnapshot.signature !== snapshot.signature; if (!forceScroll && (!contentChanged || !wasPinnedToBottom)) { return; } const animationFrameId = window.requestAnimationFrame(() => { void scrollToBottom({ animation: "instant", ignoreEscapes: forceScroll, wait: !forceScroll }); }); return () => window.cancelAnimationFrame(animationFrameId); }, [scrollToBottom, snapshot, wasPinnedToBottom]); return null; } 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; } function createAgentConversationScrollSignature( messages: AgentChatMessage[], streaming: boolean, uiResults: AgentUiResult[] ) { return [ streaming ? "streaming" : "idle", messages.map(createAgentMessageScrollSignature).join("|"), uiResults.map((result) => result.id).join("|") ].join("#"); } function createAgentMessageScrollSignature(message: AgentChatMessage) { return [ message.id, message.role, message.content.length, message.progress?.map((item) => `${item.id}:${item.status}:${item.title.length}:${item.detail?.length ?? 0}`).join(",") ?? "", message.todos?.todos.map((todo) => `${todo.id}:${todo.status}:${todo.content.length}`).join(",") ?? "", message.permissions?.map((permission) => `${permission.requestId}:${permission.status}:${permission.error?.length ?? 0}`).join(",") ?? "", message.questions?.map((question) => `${question.requestId}:${question.status}:${question.error?.length ?? 0}`).join(",") ?? "" ].join(":"); } function AgentModelSelect({ models, value, onValueChange }: { models: AgentModelOption[]; value: string; onValueChange?: (model: string) => void; }) { const selectedModel = models.find((model) => model.id === value) ?? models[0]; if (!models.length) { return ( 模型 ); } return ( 模型选择
{models.map((model) => { const selected = model.id === selectedModel?.id; return ( onValueChange?.(model.id)} > {model.label} {model.description} {selected ? ); })}
); } function getAgentConnectionLabel(statusLabel: string) { return /失败|不可用|错误|降级/.test(statusLabel) ? "离线" : "在线"; } function getAgentConnectionClassName(statusLabel: string) { return /失败|不可用|错误|降级/.test(statusLabel) ? "bg-red-500" : "bg-emerald-500"; } function ModelIcon({ model, size }: { model?: AgentModelOption; size: number }) { return model?.icon === "bolt" ? : ; } function FastModelIcon({ size }: { size: number }) { return ( ); } function ExpertModelIcon({ size }: { size: number }) { return ( ); } function AgentReadyMark() { return ( ); } function AgentEmptyState() { return (

我已就绪,请描述任务

使用自然语言下达指令,我会整理监测与空间证据,并在地图上呈现分析结果。

    {AGENT_CAPABILITIES.map(({ icon: Icon, label }) => (
  • {label}
  • ))}
); } function BackendMessageList({ messages, streaming, streamRenderState, onReplyPermission, onReplyQuestion, onRejectQuestion }: { 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; }) { return ( {messages.map((message, index) => (
{message.role === "assistant" ? ( ) : null} {message.content ? ( message.role === "assistant" ? ( ) : (

{message.content}

) ) : streaming && index === messages.length - 1 ? ( 正在生成... ) : null} {message.role === "assistant" ? ( ) : null}
))}
); }