import {
  Activity,
  CheckCircle2,
  ChevronsUpDown,
  ChevronLeft,
  DatabaseZap,
  History,
  LoaderCircle,
  MapPinned,
  Mic,
  Pause,
  Play,
  Plus,
  SendHorizontal,
  Shield,
  ShieldCheck,
  Square,
  Volume2,
  X
} from "lucide-react";
import { AnimatePresence, MotionConfig, motion, useReducedMotion } from "motion/react";
import { useCallback, useEffect, useId, useMemo, useRef, useState, type ReactNode } from "react";
import { createPortal } from "react-dom";
import { useStickToBottomContext } from "use-stick-to-bottom";
import { showMapNotice } from "@/features/map/core";
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 "@/shared/ui/cn";
import { StatusBadge, StatusDot, type StatusTone } from "@/shared/ui/status";
import type { AgentChatSessionSummary } from "../api/client";
import { AgentPersona } from "./agent-persona";
import {
  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 { useAgentSpeechRecognition, useAgentSpeechSynthesis } from "../hooks/use-agent-voice";
import {
  findSpeechSelectionStartOffset,
  stripSpeechMarkdown,
  type AgentSpeakOptions,
  type AgentSpeechState
} from "../speech";
import type {
  AgentApprovalMode,
  AgentChatMessage,
  AgentConnectionStatus,
  AgentModelOption,
  AgentPermissionReply,
  AgentPermissionRequest,
  AgentQuestionRequest,
  AgentStreamRenderState,
  AgentUiResult
} from "../types";

export type AgentCommandPanelPresentation = "desktop-dock" | "desktop-floating" | "mobile-sheet";

type AgentCommandPanelProps = {
  presentation?: AgentCommandPanelPresentation;
  collapsing?: boolean;
  personaState?: PersonaState;
  sessionTitle?: string;
  sessionHistory?: AgentChatSessionSummary[];
  sessionHistoryLoading?: boolean;
  activeSessionId?: string | null;
  statusLabel?: string;
  connectionStatus?: AgentConnectionStatus;
  streaming?: boolean;
  messages?: AgentChatMessage[];
  streamRenderState?: AgentStreamRenderState;
  modelOptions?: AgentModelOption[];
  selectedModel?: string;
  approvalMode?: AgentApprovalMode;
  uiResults?: AgentUiResult[];
  onCollapse: () => void;
  onRefreshHistory?: () => Promise<void> | void;
  onStartNewSession?: () => Promise<void> | void;
  onLoadHistorySession?: (sessionId: string) => Promise<void> | void;
  onRenameHistorySession?: (sessionId: string, title: string) => Promise<void> | void;
  onDeleteHistorySession?: (sessionId: string) => Promise<void> | void;
  onSelectModel?: (model: string) => void;
  onApprovalModeChange?: (mode: AgentApprovalMode) => void;
  onSubmitPrompt: (prompt: string) => Promise<void> | void;
  onStopPrompt?: () => void;
  onReplyPermission?: (
    permission: AgentPermissionRequest,
    reply: AgentPermissionReply
  ) => Promise<void> | void;
  onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise<void> | void;
  onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
};

const AGENT_PROMPT_SUGGESTIONS = [
  "分析当前管网中的水力瓶颈管道，并给出改造建议。",
  "供水服务分区分析。",
  "帮我分析当前管网压力异常点，并按风险等级排序。",
  "帮我生成一份今日运行简报，包含问题、原因和建议。",
  "查询关键 SCADA 点位最近 24 小时的异常波动。",
  "排查当前管网爆管风险，并说明优先处置建议。"
] as const;

const AGENT_CAPABILITIES = [
  { icon: DatabaseZap, label: "水力瓶颈诊断" },
  { icon: Activity, label: "压力异常研判" },
  { icon: ShieldCheck, label: "爆管风险排查" },
  { icon: MapPinned, label: "GIS 地图联动" }
] as const;

export function AgentCommandPanel({
  presentation = "desktop-dock",
  collapsing = false,
  personaState,
  sessionTitle = "新对话",
  sessionHistory = [],
  sessionHistoryLoading = false,
  activeSessionId,
  statusLabel = "Agent 后端连接中",
  connectionStatus = "connecting",
  streaming = false,
  messages = [],
  streamRenderState = {},
  modelOptions = [],
  selectedModel = "",
  approvalMode = "request",
  uiResults = [],
  onCollapse,
  onRefreshHistory,
  onStartNewSession,
  onLoadHistorySession,
  onRenameHistorySession,
  onDeleteHistorySession,
  onSelectModel,
  onApprovalModeChange,
  onSubmitPrompt,
  onStopPrompt,
  onReplyPermission,
  onReplyQuestion,
  onRejectQuestion
}: AgentCommandPanelProps) {
  const [prompt, setPrompt] = useState("");
  const [historyOpen, setHistoryOpen] = useState(false);
  const [loadingSessionId, setLoadingSessionId] = useState<string | null>(null);
  const [scrollRequestId, setScrollRequestId] = useState(0);
  const historyPanelId = useId();
  const historyTriggerRef = useRef<HTMLButtonElement | null>(null);
  const historyPanelRef = useRef<HTMLDivElement | null>(null);
  const desktopFloating = presentation === "desktop-floating";
  const floatingPresentation = desktopFloating || presentation === "mobile-sheet";
  const hasConversation = messages.length > 0 || streaming;
  const trimmedPrompt = prompt.trim();
  const {
    speechState,
    speakingMessageId,
    speak,
    pause: pauseSpeech,
    resume: resumeSpeech,
    stop: stopSpeech,
    isSupported: isSpeechSupported
  } = useAgentSpeechSynthesis();
  const appendRecognitionResult = useCallback((text: string) => {
    setPrompt((current) => `${current}${current.trim() ? " " : ""}${text}`);
  }, []);
  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(() => {
    if (historyOpen) {
      void Promise.resolve(onRefreshHistory?.());
    }
  }, [historyOpen, onRefreshHistory]);

  useEffect(() => {
    if (!historyOpen) {
      return;
    }

    const closeHistoryOnOutsidePointer = (event: PointerEvent) => {
      const target = event.target;
      if (!(target instanceof Node)) {
        return;
      }

      if (
        historyPanelRef.current?.contains(target) ||
        historyTriggerRef.current?.contains(target)
      ) {
        return;
      }

      setHistoryOpen(false);
    };
    const closeHistoryOnEscape = (event: KeyboardEvent) => {
      if (event.key !== "Escape") {
        return;
      }

      event.preventDefault();
      event.stopPropagation();
      setHistoryOpen(false);
      window.requestAnimationFrame(() => historyTriggerRef.current?.focus());
    };

    document.addEventListener("pointerdown", closeHistoryOnOutsidePointer, true);
    document.addEventListener("keydown", closeHistoryOnEscape);

    return () => {
      document.removeEventListener("pointerdown", closeHistoryOnOutsidePointer, true);
      document.removeEventListener("keydown", closeHistoryOnEscape);
    };
  }, [historyOpen]);

  useEffect(() => {
    stopSpeech();
  }, [activeSessionId, stopSpeech]);

  useEffect(() => {
    if (streaming) stopListening();
  }, [stopListening, streaming]);

  const submitPrompt = (nextPrompt: string) => {
    if (!nextPrompt || streaming) {
      return;
    }
    setPrompt("");
    stopListening();
    setScrollRequestId((requestId) => requestId + 1);
    void Promise.resolve(onSubmitPrompt(nextPrompt)).catch(() => {
      setPrompt(nextPrompt);
    });
  };
  const historyPanel = (
    <AgentHistoryPanel
      activeSessionId={activeSessionId}
      loadingSessionId={loadingSessionId}
      loading={sessionHistoryLoading}
      sessions={sessionHistory}
      onRefresh={onRefreshHistory}
      onSelectSession={(nextSessionId) => {
        setLoadingSessionId(nextSessionId);
        void Promise.resolve(onLoadHistorySession?.(nextSessionId))
          .then(() => setHistoryOpen(false))
          .finally(() => setLoadingSessionId(null));
      }}
      onRenameSession={onRenameHistorySession}
      onDeleteSession={onDeleteHistorySession}
    />
  );
  const operationalBrief = (
    <AgentOperationalBrief
      messages={messages}
      statusLabel={statusLabel}
      streaming={streaming}
      onSubmitCommand={submitPrompt}
    />
  );

  return (
    <MotionConfig reducedMotion="user">
      <aside
        aria-label="Agent 命令面板"
        className={cn(
          "pointer-events-auto flex h-full min-h-0 w-full flex-col",
          "agent-panel-shell",
          presentation === "desktop-dock"
            ? "acrylic-panel overflow-hidden rounded-r-2xl border-y border-r"
            : desktopFloating
              ? "acrylic-panel overflow-visible rounded-2xl border"
              : "overflow-hidden rounded-none border-0 bg-transparent",
          collapsing ? "agent-panel-collapse" : "agent-panel-enter"
        )}
      >
        <Agent
          className={cn(
            "relative flex h-full min-h-0 flex-col border-0 bg-transparent",
            floatingPresentation && "gap-1 overflow-visible p-1"
          )}
        >
          <div className="relative z-30 h-16 shrink-0">
            <header
              className={cn(
                "agent-panel-header agent-panel-header-expandable inset-x-0 top-0 overflow-hidden",
                floatingPresentation && "agent-panel-integrated-header",
                presentation === "mobile-sheet" && "agent-panel-header-mobile",
                historyOpen && "agent-panel-header-expanded"
              )}
            >
              <div className="flex min-h-16 items-center justify-between gap-2 px-3 py-2">
                <div className="flex min-w-0 flex-1 items-center gap-2.5 text-slate-900">
                  <AgentPersona className="h-12 w-12" state={personaState} />
                  <div className="min-w-0 flex-1">
                    <div className="min-w-0">
                      <div className="flex min-w-0 items-center gap-2">
                        <p className="shrink-0 text-xs font-semibold uppercase text-slate-500">
                          对话主题
                        </p>
                        <StatusBadge
                          tone={agentConnectionMetadata[connectionStatus].tone}
                          icon={
                            <StatusDot
                              tone={agentConnectionMetadata[connectionStatus].tone}
                              activity={connectionStatus === "connecting" ? "live" : "static"}
                            />
                          }
                        >
                          {agentConnectionMetadata[connectionStatus].label}
                        </StatusBadge>
                      </div>
                      <p
                        className="truncate text-sm font-semibold text-slate-950"
                        title={sessionTitle}
                      >
                        {sessionTitle}
                      </p>
                    </div>
                  </div>
                </div>
                <button
                  type="button"
                  aria-label="新建 Agent 对话"
                  title="新建 Agent 对话"
                  onClick={() => {
                    setHistoryOpen(false);
                    setPrompt("");
                    void Promise.resolve(onStartNewSession?.());
                  }}
                  className="agent-panel-icon-button grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-600"
                >
                  <Plus size={17} aria-hidden="true" />
                </button>
                <button
                  ref={historyTriggerRef}
                  type="button"
                  aria-label="打开 Agent 历史记录"
                  title="打开 Agent 历史记录"
                  aria-expanded={historyOpen}
                  aria-controls={historyPanelId}
                  onClick={() => setHistoryOpen((open) => !open)}
                  className="agent-panel-icon-button grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-600"
                >
                  <History size={17} aria-hidden="true" />
                </button>
                {presentation === "mobile-sheet" ? (
                  <button
                    type="button"
                    aria-label="关闭 Agent 面板"
                    title="关闭 Agent 面板"
                    onClick={onCollapse}
                    className="agent-panel-icon-button grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-600"
                  >
                    <X size={17} aria-hidden="true" />
                  </button>
                ) : (
                  <button
                    type="button"
                    aria-label="折叠 Agent 面板"
                    title="折叠 Agent 面板"
                    onClick={onCollapse}
                    className="agent-panel-icon-button grid h-10 w-10 shrink-0 place-items-center rounded-xl text-slate-600"
                  >
                    <ChevronLeft size={17} aria-hidden="true" />
                  </button>
                )}
              </div>
              <AnimatePresence initial={false}>
                {historyOpen ? (
                  <motion.div
                    ref={historyPanelRef}
                    key="history"
                    id={historyPanelId}
                    role="region"
                    aria-label="Agent 历史记录"
                    className="agent-panel-floating-acrylic agent-panel-history-extension"
                    initial="initial"
                    animate="animate"
                    exit="exit"
                    variants={agentPanelSectionVariants}
                    style={{ overflow: "hidden" }}
                  >
                    {historyPanel}
                  </motion.div>
                ) : null}
              </AnimatePresence>
            </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">
            <Conversation
              className={cn(
                "agent-panel-conversation min-h-0",
                floatingPresentation && "agent-panel-conversation-canvas"
              )}
            >
              <ConversationContent
                className={cn(
                  "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"
                )}
              >
                <AnimatePresence mode="popLayout" initial={false}>
                  {messages.length > 0 ? (
                    <BackendMessageList
                      key="messages"
                      messages={messages}
                      streaming={streaming}
                      streamRenderState={streamRenderState}
                      onReplyPermission={onReplyPermission}
                      onReplyQuestion={onReplyQuestion}
                      onRejectQuestion={onRejectQuestion}
                      isSpeechSupported={isSpeechSupported}
                      speakingMessageId={speakingMessageId}
                      speechState={speechState}
                      onSpeak={speak}
                      onPauseSpeech={pauseSpeech}
                      onResumeSpeech={resumeSpeech}
                      onStopSpeech={stopSpeech}
                    />
                  ) : (
                    <motion.div
                      key="empty"
                      className="flex flex-1 items-center justify-center px-4 py-8"
                      initial="initial"
                      animate="animate"
                      exit="exit"
                      variants={agentPanelItemVariants}
                    >
                      <AgentEmptyState compact={presentation === "mobile-sheet"} />
                    </motion.div>
                  )}
                </AnimatePresence>

                <AgentUiEnvelopeRenderer results={uiResults} />
              </ConversationContent>
              <AgentConversationScrollManager
                activeSessionId={activeSessionId}
                messages={messages}
                scrollRequestId={scrollRequestId}
                streaming={streaming}
              />
              <ConversationScrollButton
                className={cn(
                  "agent-panel-floating-acrylic agent-conversation-scroll-button",
                  floatingPresentation ? "bottom-[10.25rem] z-30" : "bottom-3"
                )}
              />
            </Conversation>
          </AgentContent>

          <div
            className={cn(
              "agent-panel-band relative",
              floatingPresentation
                ? "agent-panel-floating-acrylic agent-panel-composer absolute bottom-3 z-20 rounded-2xl p-1"
                : "border-t border-slate-200 p-3"
            )}
          >
            {!hasConversation ? (
              <div
                className={cn(
                  "agent-panel-conversation pointer-events-none absolute inset-x-0 bottom-full z-10 px-3 pb-2 pt-2",
                  floatingPresentation && "bg-transparent"
                )}
              >
                <Suggestions
                  aria-label="推荐问题"
                  role="group"
                  className="pointer-events-auto px-0.5"
                >
                  {AGENT_PROMPT_SUGGESTIONS.map((suggestion) => (
                    <Suggestion
                      key={suggestion}
                      suggestion={suggestion}
                      onClick={submitPrompt}
                      className="surface-reading max-w-full border border-slate-200 text-slate-600 hover:bg-blue-50 hover:text-blue-700"
                    >
                      <span className="truncate">{suggestion}</span>
                    </Suggestion>
                  ))}
                </Suggestions>
              </div>
            ) : null}
            <PromptInput
              className="agent-panel-control overflow-hidden rounded-2xl shadow-xs [&>[data-slot=input-group]]:rounded-[inherit] [&>[data-slot=input-group]]:border-0 [&>[data-slot=input-group]]:shadow-none [&>[data-slot=input-group]]:ring-0!"
              onSubmit={(message) => {
                const nextPrompt = message.text.trim();
                if (nextPrompt) {
                  submitPrompt(nextPrompt);
                }
              }}
            >
              <PromptInputBody>
                <PromptInputTextarea
                  value={prompt}
                  onChange={(event) => setPrompt(event.currentTarget.value)}
                  className={cn("text-sm", floatingPresentation ? "min-h-20" : "min-h-14")}
                  placeholder="输入调度问题，Agent 将通过后端会话流式响应"
                />
              </PromptInputBody>
              <PromptInputFooter className="justify-between gap-2">
                <div className="flex min-w-0 items-center gap-2">
                  <ApprovalModeSelect
                    value={approvalMode}
                    disabled={streaming}
                    onValueChange={onApprovalModeChange}
                  />
                </div>
                <div className="flex shrink-0 items-center gap-2">
                  <AgentModelSelect
                    models={modelOptions}
                    value={selectedModel}
                    onValueChange={onSelectModel}
                  />
                  {isRecognitionSupported ? (
                    <VoiceInputButton
                      isListening={isListening}
                      disabled={streaming}
                      onClick={
                        isListening ? stopListening : () => startListening(showRecognitionError)
                      }
                    />
                  ) : null}
                  <PromptInputSubmit
                    aria-label={streaming ? "停止 Agent 指令" : "发送 Agent 指令"}
                    title={streaming ? "停止 Agent 指令" : "发送 Agent 指令"}
                    className="agent-panel-primary-icon"
                    disabled={!streaming && !trimmedPrompt}
                    status={streaming ? "streaming" : "ready"}
                    onStop={onStopPrompt}
                  >
                    {streaming ? undefined : (
                      <SendHorizontal
                        size={15}
                        style={{ transform: "rotate(-45deg)" }}
                        aria-hidden="true"
                      />
                    )}
                  </PromptInputSubmit>
                </div>
              </PromptInputFooter>
            </PromptInput>
          </div>
        </Agent>
      </aside>
    </MotionConfig>
  );
}

const VOICE_WAVE_BARS = [
  { height: 5, duration: 0.72, delay: 0.08 },
  { height: 10, duration: 0.58, delay: 0 },
  { height: 15, duration: 0.66, delay: 0.12 },
  { height: 9, duration: 0.62, delay: 0.04 },
  { height: 4, duration: 0.7, delay: 0.16 }
] as const;

function VoiceInputButton({
  isListening,
  disabled,
  onClick
}: {
  isListening: boolean;
  disabled: boolean;
  onClick: () => void;
}) {
  const reduceMotion = useReducedMotion();
  const label = isListening ? "停止语音输入" : "语音输入";

  return (
    <motion.button
      type="button"
      aria-label={label}
      aria-pressed={isListening}
      title={label}
      className={cn(
        "group relative grid h-8 w-8 shrink-0 place-items-center overflow-visible rounded-lg border after:absolute after:-inset-1 after:content-['']",
        "border-transparent bg-transparent shadow-none",
        "transition-[color,background-color,border-color,box-shadow,transform] duration-200",
        "focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25 focus-visible:ring-offset-1",
        "disabled:cursor-not-allowed disabled:opacity-45",
        isListening
          ? "status-tone-danger bg-[var(--status-soft)] text-[var(--status-foreground)] [@media(hover:hover)]:hover:border-[var(--status-border)] [@media(hover:hover)]:hover:brightness-95"
          : "text-slate-600 [@media(hover:hover)]:hover:border-[var(--action-border-hover)] [@media(hover:hover)]:hover:bg-[var(--action-soft)] [@media(hover:hover)]:hover:text-[var(--action-blue-hover)]"
      )}
      disabled={disabled}
      onClick={onClick}
    >
      {isListening ? (
        <motion.span
          data-slot="voice-input-pulse"
          className="pointer-events-none absolute inset-0 rounded-lg bg-[var(--status-mark)] opacity-20"
          aria-hidden="true"
          initial={{ opacity: 0, scale: 0.9 }}
          animate={
            reduceMotion
              ? { opacity: 0.38, scale: 1 }
              : { opacity: [0, 0.4, 0], scale: [0.9, 1, 1.28] }
          }
          transition={
            reduceMotion
              ? { duration: 0.16 }
              : {
                  duration: 1.7,
                  ease: "easeOut",
                  repeat: Infinity,
                  times: [0, 0.12, 1]
                }
          }
        />
      ) : null}

      <span className="relative grid h-5 w-5 place-items-center overflow-hidden">
        <AnimatePresence initial={false} mode="wait">
          {isListening ? (
            <motion.span
              key="voice-wave"
              className="flex h-4 items-center gap-0.5"
              aria-hidden="true"
              initial={{ opacity: 0, scale: 0.72 }}
              animate={{ opacity: 1, scale: 1 }}
              exit={{ opacity: 0, scale: 0.72 }}
              transition={{ duration: 0.16, ease: "easeOut" }}
            >
              {VOICE_WAVE_BARS.map((bar) => (
                <motion.span
                  key={bar.height}
                  className="w-px rounded-full bg-[var(--status-mark)]"
                  style={{ height: bar.height }}
                  animate={
                    reduceMotion ? { scaleY: 0.72 } : { scaleY: [0.38, 1, 0.52, 0.82, 0.38] }
                  }
                  transition={
                    reduceMotion
                      ? { duration: 0.16 }
                      : {
                          duration: bar.duration,
                          delay: bar.delay,
                          ease: "easeInOut",
                          repeat: Infinity
                        }
                  }
                  aria-hidden="true"
                />
              ))}
            </motion.span>
          ) : (
            <motion.span
              key="voice-microphone"
              className="grid place-items-center"
              aria-hidden="true"
              initial={{ opacity: 0, scale: 0.72, y: 2 }}
              animate={{ opacity: 1, scale: 1, y: 0 }}
              exit={{ opacity: 0, scale: 0.72, y: -2 }}
              transition={{ duration: 0.16, ease: "easeOut" }}
            >
              <Mic
                size={15}
                strokeWidth={2.15}
                className="transition-transform duration-200 group-hover:scale-105"
              />
            </motion.span>
          )}
        </AnimatePresence>
      </span>
    </motion.button>
  );
}

function ApprovalModeSelect({
  value,
  disabled,
  onValueChange
}: {
  value: AgentApprovalMode;
  disabled: boolean;
  onValueChange?: (mode: AgentApprovalMode) => void;
}) {
  const requestApproval = value === "request";

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          variant="outline"
          size="sm"
          className="shrink-0 gap-1.5 border-transparent bg-transparent px-1.5 text-xs text-slate-600 shadow-none hover:bg-slate-50"
          disabled={disabled || !onValueChange}
          aria-label="权限批准模式"
        >
          {requestApproval ? (
            <Shield size={14} aria-hidden="true" />
          ) : (
            <ShieldCheck size={14} aria-hidden="true" />
          )}
          <span>{requestApproval ? "请求批准" : "始终允许"}</span>
          <ChevronsUpDown size={12} className="opacity-50" aria-hidden="true" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent
        side="top"
        align="start"
        sideOffset={8}
        className="agent-panel-control w-[220px] rounded-xl p-2 shadow-lg"
      >
        <DropdownMenuLabel className="px-2 pb-2 pt-1 text-xs text-slate-500">
          权限批准模式
        </DropdownMenuLabel>
        <DropdownMenuItem
          className={cn(
            "items-start rounded-lg border border-transparent px-2.5 py-2 focus:bg-slate-50",
            requestApproval && "border-blue-200 bg-blue-50 focus:bg-blue-50"
          )}
          onSelect={() => onValueChange?.("request")}
        >
          <Shield className="mt-0.5 text-blue-600" aria-hidden="true" />
          <span>
            <span className="block text-sm font-semibold text-slate-800">请求批准</span>
            <span className="block text-xs leading-5 text-slate-500">工具权限逐次确认</span>
          </span>
          {requestApproval ? (
            <CheckCircle2 className="ml-auto mt-0.5 text-blue-600" aria-hidden="true" />
          ) : null}
        </DropdownMenuItem>
        <DropdownMenuItem
          className={cn(
            "mt-1 items-start rounded-lg border border-transparent px-2.5 py-2 focus:bg-slate-50",
            !requestApproval &&
              "status-tone-success border-[var(--status-border)] bg-[var(--status-soft)] focus:bg-[var(--status-soft)]"
          )}
          onSelect={() => onValueChange?.("always")}
        >
          <ShieldCheck
            className="status-tone-success mt-0.5 text-[var(--status-foreground)]"
            aria-hidden="true"
          />
          <span>
            <span className="block text-sm font-semibold text-slate-800">始终允许</span>
            <span className="block text-xs leading-5 text-slate-500">自动允许本轮权限请求</span>
          </span>
          {!requestApproval ? (
            <CheckCircle2
              className="status-tone-success ml-auto mt-0.5 text-[var(--status-foreground)]"
              aria-hidden="true"
            />
          ) : null}
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

type AgentConversationScrollSnapshot = {
  activeSessionId?: string | null;
  lastMessageRevision: string;
  lastUserMessageId: string | null;
  scrollRequestId: number;
  streaming: boolean;
};

function AgentConversationScrollManager({
  activeSessionId,
  messages,
  scrollRequestId,
  streaming
}: {
  activeSessionId?: string | null;
  messages: AgentChatMessage[];
  scrollRequestId: number;
  streaming: boolean;
}) {
  const { scrollToBottom } = useStickToBottomContext();
  const previousSnapshotRef = useRef<AgentConversationScrollSnapshot | null>(null);
  const lastUserMessageId = getLastUserMessageId(messages);
  const lastMessageRevision = getLastMessageRevision(messages);
  const snapshot = useMemo<AgentConversationScrollSnapshot>(
    () => ({
      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 ||
      (Boolean(snapshot.lastUserMessageId) &&
        previousSnapshot.lastUserMessageId !== snapshot.lastUserMessageId) ||
      (snapshot.streaming &&
        previousSnapshot.lastMessageRevision !== snapshot.lastMessageRevision) ||
      previousSnapshot.streaming !== snapshot.streaming;

    if (!forceScroll) {
      return;
    }

    const animationFrameId = window.requestAnimationFrame(() => {
      void scrollToBottom({
        animation: "instant",
        ignoreEscapes: true,
        wait: false
      });
    });

    return () => window.cancelAnimationFrame(animationFrameId);
  }, [scrollToBottom, snapshot]);

  return 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;
}

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 (
      <span className="shrink-0 rounded-md px-2 py-1 text-xs font-medium text-slate-500">模型</span>
    );
  }

  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button
          type="button"
          variant="outline"
          size="sm"
          className="w-24 shrink-0 justify-between gap-1.5 border-transparent bg-transparent px-1.5 text-xs text-slate-600 shadow-none hover:bg-slate-50"
          disabled={!onValueChange}
          aria-label="选择 Agent 模型"
        >
          <span className="flex min-w-0 flex-1 items-center gap-1">
            <ModelIcon model={selectedModel} size={selectedModel?.icon === "bolt" ? 15 : 14} />
            <span className="shrink-0">{selectedModel?.label ?? "模型"}</span>
          </span>
          <ChevronsUpDown size={12} className="shrink-0 opacity-50" aria-hidden="true" />
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent
        side="top"
        align="end"
        sideOffset={8}
        className="agent-panel-control w-[230px] rounded-xl p-2 shadow-lg"
      >
        <DropdownMenuLabel className="px-2 pb-2 pt-1 text-xs text-slate-500">
          模型选择
        </DropdownMenuLabel>
        <div className="space-y-1">
          {models.map((model) => {
            const selected = model.id === selectedModel?.id;
            return (
              <DropdownMenuItem
                key={model.id}
                className={cn(
                  "rounded-lg border border-transparent px-2.5 py-2 focus:bg-slate-50",
                  selected && "border-violet-200 bg-violet-50 focus:bg-violet-50"
                )}
                onSelect={() => onValueChange?.(model.id)}
              >
                <span className="ml-0.5 shrink-0 pt-0.5">
                  <ModelIcon model={model} size={model.icon === "bolt" ? 18 : 16} />
                </span>
                <span className="min-w-0 flex-1">
                  <span className="block truncate text-sm font-semibold text-slate-800">
                    {model.label}
                  </span>
                  <span className="block truncate text-xs leading-4 text-slate-500">
                    {model.description}
                  </span>
                </span>
                {selected ? (
                  <CheckCircle2 size={15} className="text-violet-600" aria-hidden="true" />
                ) : null}
              </DropdownMenuItem>
            );
          })}
        </div>
      </DropdownMenuContent>
    </DropdownMenu>
  );
}

const agentConnectionMetadata = {
  connecting: { label: "连接中", tone: "info" },
  online: { label: "在线", tone: "success" },
  offline: { label: "离线", tone: "danger" }
} satisfies Record<AgentConnectionStatus, { label: string; tone: StatusTone }>;

function ModelIcon({ model, size }: { model?: AgentModelOption; size: number }) {
  return model?.icon === "bolt" ? <FastModelIcon size={size} /> : <ExpertModelIcon size={size} />;
}

function FastModelIcon({ size }: { size: number }) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      className="text-sky-600"
      aria-hidden="true"
    >
      <path
        d="M10.67 21c-.35 0-.62-.31-.57-.66L11 14H7.5c-.88 0-.33-.75-.31-.78 1.26-2.23 3.15-5.53 5.65-9.93.1-.18.3-.29.5-.29.35 0 .62.31.57.66l-.9 6.34h3.51c.4 0 .62.19.4.66-3.29 5.74-5.2 9.09-5.75 10.05-.1.18-.29.29-.5.29"
        fill="currentColor"
      />
    </svg>
  );
}

function ExpertModelIcon({ size }: { size: number }) {
  return (
    <svg
      width={size}
      height={size}
      viewBox="0 0 24 24"
      fill="none"
      className="text-violet-600"
      aria-hidden="true"
    >
      <path
        d="m19.46 8 .79-1.75L22 5.46c.39-.18.39-.73 0-.91l-1.75-.79L19.46 2c-.18-.39-.73-.39-.91 0l-.79 1.75-1.76.79c-.39.18-.39.73 0 .91l1.75.79.79 1.76c.18.39.74.39.92 0M11.5 9.5 9.91 6c-.35-.78-1.47-.78-1.82 0L6.5 9.5 3 11.09c-.78.36-.78 1.47 0 1.82l3.5 1.59L8.09 18c.36.78 1.47.78 1.82 0l1.59-3.5 3.5-1.59c.78-.36.78-1.47 0-1.82zm7.04 6.5-.79 1.75-1.75.79c-.39.18-.39.73 0 .91l1.75.79.79 1.76c.18.39.73.39.91 0l.79-1.75 1.76-.79c.39-.18.39-.73 0-.91l-1.75-.79-.79-1.76c-.18-.39-.74-.39-.92 0"
        fill="currentColor"
      />
    </svg>
  );
}

function AgentReadyMark() {
  return (
    <svg viewBox="0 0 1024 1024" className="h-12 w-12 overflow-visible" aria-hidden="true">
      <g className="agent-ready-orbit">
        <path
          d="M384.1536 952.1664a38.4 38.4 0 0 1-49.3568 22.528 498.3808 498.3808 0 0 1-284.928-273.92 38.4 38.4 0 0 1 70.8608-29.6448 421.5808 421.5808 0 0 0 240.896 231.6288 38.4 38.4 0 0 1 22.528 49.408zM952.1152 384.9728a38.4 38.4 0 0 1-49.4592-22.528 421.5296 421.5296 0 0 0-234.1376-241.5104 38.4 38.4 0 0 1 29.184-71.0656 498.3296 498.3296 0 0 1 276.8896 285.696 38.4 38.4 0 0 1-22.528 49.408z"
          fill="#CE75FF"
        />
        <path
          d="M981.248 768.0512a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.2992 0zM127.9488 256.0512a42.6496 42.6496 0 1 1-85.3504 0 42.6496 42.6496 0 0 1 85.3504 0z"
          fill="#F62E76"
        />
        <path
          d="M810.496 938.8544a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.3504 0zM298.496 85.504a42.6496 42.6496 0 1 1-85.2992 0 42.6496 42.6496 0 0 1 85.3504 0z"
          fill="#CD88FF"
        />
      </g>
      <path
        className="agent-ready-core"
        d="M511.9488 276.736l-27.8528 114.7392A126.0544 126.0544 0 0 1 391.3216 484.352l-114.7904 27.8528 114.7904 27.8016a126.0544 126.0544 0 0 1 92.7744 92.8256L512 747.52l27.8016-114.7392a126.0544 126.0544 0 0 1 92.8256-92.8256l114.7392-27.8016-114.7392-27.8528a126.0544 126.0544 0 0 1-92.8256-92.8256L512 276.736z m55.6544-62.1568c-14.1312-58.368-97.1776-58.368-111.36 0L417.28 375.296a57.344 57.344 0 0 1-42.1888 42.1888l-160.6656 38.912c-58.4192 14.1824-58.4192 97.28 0 111.4112l160.6656 38.9632c20.8384 5.12 37.12 21.3504 42.1888 42.1888l38.9632 160.7168c14.1824 58.368 97.2288 58.368 111.36 0l38.9632-160.7168a57.344 57.344 0 0 1 42.1888-42.1888l160.7168-38.912c58.368-14.1824 58.368-97.28 0-111.4112l-160.7168-38.9632a57.344 57.344 0 0 1-42.1888-42.1888l-38.912-160.7168z"
        fill="#F3E2FF"
      />
    </svg>
  );
}

function AgentEmptyState({ compact = false }: { compact?: boolean }) {
  return (
    <section
      aria-labelledby="agent-empty-state-title"
      className="mx-auto w-full max-w-[360px]"
      lang="zh-CN"
    >
      <div className={cn("px-2", compact ? "py-2" : "py-4")}>
        <div className="flex items-center gap-4">
          <div
            className={cn(
              "surface-control grid shrink-0 place-items-center rounded-xl shadow-[inset_0_0_0_1px_oklch(0.84_0.025_260)]",
              compact ? "h-14 w-14" : "h-[72px] w-[72px]"
            )}
          >
            <AgentReadyMark />
          </div>
          <div className="min-w-0 flex-1 text-left">
            <h2
              id="agent-empty-state-title"
              className={cn(
                "text-balance font-semibold text-slate-950",
                compact ? "text-base leading-6" : "text-lg leading-7"
              )}
            >
              我已就绪，请描述任务
            </h2>
          </div>
        </div>

        <p
          className={cn(
            "text-pretty text-left text-sm leading-6 text-[#334155]",
            compact ? "mt-3" : "mt-5"
          )}
        >
          直接说出你想调查的问题，我会整理监测与空间证据，并将分析结果带回地图。
        </p>
      </div>

      <ul
        className={cn(
          "surface-control grid grid-cols-2 overflow-hidden rounded-xl shadow-[inset_0_0_0_1px_rgba(148,163,184,0.32)]",
          compact && "hidden"
        )}
        aria-label="Agent 分析能力"
      >
        {AGENT_CAPABILITIES.map(({ icon: Icon, label }, index) => (
          <li
            key={label}
            className={cn(
              "flex min-h-20 items-center gap-3 px-3.5 py-3 text-left",
              index % 2 === 0 && "border-r border-slate-300/80",
              index < 2 && "border-b border-slate-300/80"
            )}
          >
            <span className="grid h-9 w-9 shrink-0 place-items-center rounded-xl bg-[#f9fbfe] text-[#334155] shadow-[inset_0_0_0_1px_rgba(148,163,184,0.28)]">
              <Icon size={16} strokeWidth={1.8} aria-hidden="true" />
            </span>
            <span className="text-sm font-semibold leading-5 text-[#1e293b]">{label}</span>
          </li>
        ))}
      </ul>
    </section>
  );
}

function BackendMessageList({
  messages,
  streaming,
  streamRenderState,
  onReplyPermission,
  onReplyQuestion,
  onRejectQuestion,
  isSpeechSupported,
  speakingMessageId,
  speechState,
  onSpeak,
  onPauseSpeech,
  onResumeSpeech,
  onStopSpeech
}: {
  messages: AgentChatMessage[];
  streaming: boolean;
  streamRenderState: AgentStreamRenderState;
  onReplyPermission?: (
    permission: AgentPermissionRequest,
    reply: AgentPermissionReply
  ) => Promise<void> | void;
  onReplyQuestion?: (request: AgentQuestionRequest, answers: string[][]) => Promise<void> | void;
  onRejectQuestion?: (request: AgentQuestionRequest) => Promise<void> | void;
  isSpeechSupported: boolean;
  speakingMessageId: string | null;
  speechState: AgentSpeechState;
  onSpeak: (messageId: string, text: string, options?: AgentSpeakOptions) => Promise<void> | void;
  onPauseSpeech: () => void;
  onResumeSpeech: () => void;
  onStopSpeech: () => void;
}) {
  return (
    <motion.div
      className="flex w-full flex-col gap-4"
      variants={agentPanelListVariants}
      animate="animate"
    >
      <AnimatePresence initial={false} mode="popLayout">
        {messages.map((message, index) => (
          <motion.div
            key={message.id}
            className={cn("w-full", message.role === "user" && "flex justify-end")}
            initial="initial"
            animate="animate"
            exit="exit"
            variants={agentPanelItemVariants}
          >
            <Message
              from={message.role}
              className={message.role === "user" ? "max-w-[92%]" : "max-w-full"}
            >
              <MessageContent
                className={cn(
                  message.role === "user" &&
                    "agent-panel-user-message rounded-2xl px-3 py-3 text-slate-800",
                  message.role === "assistant" &&
                    "agent-panel-message w-full rounded-2xl p-3 text-sm leading-6 text-slate-700"
                )}
              >
                <div className="space-y-3">
                  {message.role === "assistant" ? (
                    <AgentMessageProgress
                      progress={message.progress}
                      running={streaming && index === messages.length - 1}
                    />
                  ) : null}
                  {message.content ? (
                    message.role === "assistant" ? (
                      <AgentSpeechMessage
                        messageId={message.id}
                        content={message.content}
                        disabled={streaming && index === messages.length - 1}
                        isSupported={isSpeechSupported}
                        speechState={speakingMessageId === message.id ? speechState : "idle"}
                        onSpeak={onSpeak}
                        onPause={onPauseSpeech}
                        onResume={onResumeSpeech}
                        onStop={onStopSpeech}
                      >
                        <StreamingTokenResponse
                          className="leading-6"
                          content={message.content}
                          messageId={message.id}
                          streamDone={streamRenderState[message.id]?.done ?? !streaming}
                          streaming={streaming && index === messages.length - 1}
                        />
                      </AgentSpeechMessage>
                    ) : (
                      <p className="whitespace-pre-wrap">{message.content}</p>
                    )
                  ) : streaming && index === messages.length - 1 ? (
                    <span className="text-slate-500">正在生成...</span>
                  ) : null}
                  {message.role === "assistant" ? (
                    <AgentMessageDetails
                      message={message}
                      onReplyPermission={onReplyPermission}
                      onReplyQuestion={onReplyQuestion}
                      onRejectQuestion={onRejectQuestion}
                    />
                  ) : null}
                </div>
              </MessageContent>
            </Message>
          </motion.div>
        ))}
      </AnimatePresence>
    </motion.div>
  );
}

type SpeechSelection = {
  startOffset: number;
  left: number;
  top: number;
};

function AgentSpeechMessage({
  messageId,
  content,
  disabled,
  isSupported,
  speechState,
  onSpeak,
  onPause,
  onResume,
  onStop,
  children
}: {
  messageId: string;
  content: string;
  disabled: boolean;
  isSupported: boolean;
  speechState: AgentSpeechState;
  onSpeak: (messageId: string, text: string, options?: AgentSpeakOptions) => Promise<void> | void;
  onPause: () => void;
  onResume: () => void;
  onStop: () => void;
  children: ReactNode;
}) {
  const contentRef = useRef<HTMLDivElement | null>(null);
  const lastSelectionRef = useRef<SpeechSelection | null>(null);
  const [selection, setSelection] = useState<SpeechSelection | null>(null);
  const speechText = useMemo(() => stripSpeechMarkdown(content), [content]);
  const selectionAnchor = selection ?? lastSelectionRef.current;

  if (selection) {
    lastSelectionRef.current = selection;
  }

  const captureSelection = useCallback(() => {
    if (!isSupported || disabled) {
      setSelection(null);
      return;
    }

    const browserSelection = window.getSelection();
    const container = contentRef.current;
    if (
      !browserSelection ||
      browserSelection.rangeCount === 0 ||
      browserSelection.isCollapsed ||
      !container
    ) {
      setSelection(null);
      return;
    }

    const range = browserSelection.getRangeAt(0);
    if (!container.contains(range.commonAncestorContainer)) {
      setSelection(null);
      return;
    }

    const startOffset = findSpeechSelectionStartOffset(speechText, browserSelection.toString());
    const rect = range.getBoundingClientRect();
    if (startOffset === null || (!rect.width && !rect.height)) {
      setSelection(null);
      return;
    }

    setSelection({
      startOffset,
      left: Math.min(Math.max(rect.left + rect.width / 2, 88), window.innerWidth - 88),
      top: Math.max(rect.top - 10, 48)
    });
  }, [disabled, isSupported, speechText]);

  useEffect(() => {
    setSelection(null);
  }, [messageId, speechText]);

  useEffect(() => {
    if (!selection) return;
    const close = () => setSelection(null);
    const closeCollapsedSelection = () => {
      if (window.getSelection()?.isCollapsed) close();
    };
    const closeOnEscape = (event: KeyboardEvent) => {
      if (event.key === "Escape") close();
    };

    document.addEventListener("selectionchange", closeCollapsedSelection);
    document.addEventListener("keydown", closeOnEscape);
    window.addEventListener("resize", close);
    window.addEventListener("scroll", close, true);
    return () => {
      document.removeEventListener("selectionchange", closeCollapsedSelection);
      document.removeEventListener("keydown", closeOnEscape);
      window.removeEventListener("resize", close);
      window.removeEventListener("scroll", close, true);
    };
  }, [selection]);

  const speakFromSelection = () => {
    if (!selection) return;
    void Promise.resolve(onSpeak(messageId, speechText, { startOffset: selection.startOffset }));
    window.getSelection()?.removeAllRanges();
    setSelection(null);
  };

  return (
    <div>
      <div
        ref={contentRef}
        onPointerUp={() => window.requestAnimationFrame(captureSelection)}
        onKeyUp={() => window.requestAnimationFrame(captureSelection)}
      >
        {children}
      </div>

      {selectionAnchor && typeof document !== "undefined"
        ? createPortal(
            <div
              className="pointer-events-none fixed z-[80] -translate-x-1/2 -translate-y-full"
              style={{ left: selectionAnchor.left, top: selectionAnchor.top }}
            >
              <AnimatePresence>
                {selection ? (
                  <motion.button
                    key="speech-selection-action"
                    type="button"
                    className="agent-panel-floating-acrylic agent-speech-selection-action pointer-events-auto inline-flex h-10 items-center gap-2 rounded-xl px-2.5 pr-3 text-xs font-semibold text-slate-800 transition-colors hover:text-blue-700 focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-blue-600/25"
                    initial={{ opacity: 0 }}
                    animate={{
                      opacity: 1,
                      transition: { duration: 0.12, ease: [0.16, 1, 0.3, 1] }
                    }}
                    exit={{
                      opacity: 0,
                      transition: { duration: 0.08, ease: [0.4, 0, 1, 1] }
                    }}
                    onMouseDown={(event) => event.preventDefault()}
                    onClick={speakFromSelection}
                  >
                    <span
                      className="grid h-6 w-6 shrink-0 place-items-center rounded-lg bg-blue-50/75 text-blue-600"
                      aria-hidden="true"
                    >
                      <Volume2 size={15} />
                    </span>
                    从这里开始朗读
                  </motion.button>
                ) : null}
              </AnimatePresence>
            </div>,
            document.body
          )
        : null}

      {isSupported && !disabled ? (
        <div className="mt-2 flex min-h-8 items-center gap-1 border-t border-slate-100 pt-1.5">
          {speechState === "idle" ? (
            <SpeechIconButton
              label="语音播放"
              onClick={() => void Promise.resolve(onSpeak(messageId, speechText))}
            >
              <Volume2 size={15} aria-hidden="true" />
            </SpeechIconButton>
          ) : null}
          {speechState === "loading" ? (
            <>
              <SpeechIconButton label="正在生成语音" disabled>
                <LoaderCircle size={15} className="animate-spin text-blue-600" aria-hidden="true" />
              </SpeechIconButton>
              <SpeechIconButton label="停止语音播放" tone="danger" onClick={onStop}>
                <Square size={14} fill="currentColor" aria-hidden="true" />
              </SpeechIconButton>
            </>
          ) : null}
          {speechState === "playing" ? (
            <>
              <SpeechIconButton label="暂停语音播放" tone="primary" onClick={onPause}>
                <Pause size={15} fill="currentColor" aria-hidden="true" />
              </SpeechIconButton>
              <SpeechIconButton label="停止语音播放" tone="danger" onClick={onStop}>
                <Square size={14} fill="currentColor" aria-hidden="true" />
              </SpeechIconButton>
            </>
          ) : null}
          {speechState === "paused" ? (
            <>
              <SpeechIconButton label="继续语音播放" tone="primary" onClick={onResume}>
                <Play size={15} fill="currentColor" aria-hidden="true" />
              </SpeechIconButton>
              <SpeechIconButton label="停止语音播放" tone="danger" onClick={onStop}>
                <Square size={14} fill="currentColor" aria-hidden="true" />
              </SpeechIconButton>
            </>
          ) : null}
          <span className="text-[11px] text-slate-400">
            {speechState === "idle"
              ? "语音播放"
              : speechState === "loading"
                ? "正在生成"
                : speechState === "playing"
                  ? "播放中"
                  : "已暂停"}
          </span>
        </div>
      ) : null}
    </div>
  );
}

function SpeechIconButton({
  label,
  tone = "default",
  disabled = false,
  onClick,
  children
}: {
  label: string;
  tone?: "default" | "primary" | "danger";
  disabled?: boolean;
  onClick?: () => void;
  children: ReactNode;
}) {
  return (
    <Button
      type="button"
      aria-label={label}
      title={label}
      disabled={disabled}
      onClick={onClick}
      size="icon-sm"
      variant="ghost"
      className={cn(
        "shrink-0 text-slate-500 hover:bg-slate-100 disabled:cursor-wait",
        tone === "primary" && "text-blue-600",
        tone === "danger" && "status-tone-danger text-[var(--status-foreground)]"
      )}
    >
      {children}
    </Button>
  );
}
