"use client";

import type React from "react";
import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
import {
  AudioLines,
  Braces,
  Check,
  Copy,
  ImageIcon,
  Loader2,
  MessageSquareText,
  Mic,
  Orbit,
  PhoneOff,
  ScanLine,
  Send,
  Smartphone,
  Sparkles,
  Video,
  Waves,
  Wrench,
  X,
} from "lucide-react";

import {
  AutoTestLivePanel,
  AutoTestRunBar,
  DebugModeTabs,
  useAutoTestRunner,
} from "@/components/assistant-editor/debug-auto-test";
import { NetworkQualityIndicator } from "@/components/network-quality-indicator";
import { ClientMessageDialog } from "@/components/client-message-dialog";
import { AuraVisualizer } from "@/components/ui/aura-visualizer";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { NebulaVisualizer } from "@/components/ui/nebula-visualizer";
import {
  Popover,
  PopoverContent,
  PopoverTrigger,
} from "@/components/ui/popover";
import {
  Select,
  SelectContent,
  SelectItem,
  SelectTrigger,
  SelectValue,
} from "@/components/ui/select";
import { SpectrumVisualizer } from "@/components/ui/spectrum-visualizer";
import { Textarea } from "@/components/ui/textarea";
import { WaveVisualizer } from "@/components/ui/wave-visualizer";
import { WaveformTimelinePanel } from "@/components/ui/waveform-timeline";
import { useCameraPreview, type CameraPreview } from "@/hooks/use-camera-preview";
import { usePhotoCaptureTool } from "@/hooks/use-photo-capture-tool";
import {
  createUserInputId,
  useVoicePreview,
  type ChatMessage,
  type ClientToolDefinition,
  type UserInputPart,
  type VoicePreview,
  type VoicePreviewStatus,
} from "@/hooks/use-voice-preview";
import {
  inputAssetsApi,
  type DynamicVariableDefinition,
} from "@/lib/api";

type VizStyle = "aura" | "nebula" | "bars" | "wave";

// 调试面板顶部主视图:聊天记录 / 视频流
type DebugView = "chat" | "video";
type DebugInputMode = "mic" | "text";
type DebugMode = "manual" | "auto";
type PendingDebugImage = {
  file: File;
  previewUrl: string;
};

const DEBUG_IMAGE_MAX_BYTES = 10 * 1024 * 1024;

function fileToDataUrl(file: File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();
    reader.onload = () => resolve(String(reader.result));
    reader.onerror = () => reject(new Error("无法读取图片"));
    reader.readAsDataURL(file);
  });
}

function hasDraggedImage(dataTransfer: DataTransfer): boolean {
  return (
    Array.from(dataTransfer.items).some((item) =>
      item.type.startsWith("image/"),
    ) ||
    Array.from(dataTransfer.files).some((file) =>
      file.type.startsWith("image/"),
    )
  );
}

const VIZ_OPTIONS: { style: VizStyle; label: string; icon: React.ReactNode }[] =
  [
    { style: "aura", label: "光环", icon: <Orbit size={14} /> },
    { style: "nebula", label: "星云", icon: <Sparkles size={14} /> },
    { style: "bars", label: "频谱", icon: <AudioLines size={14} /> },
    { style: "wave", label: "波形", icon: <Waves size={14} /> },
  ];

// 中央语音可视化(光环/星云/频谱/波形)暂时隐藏:调试面板固定为
// 「上聊天记录 + 下波形监控」布局。置 true 可恢复可视化视图与样式切换。
const SHOW_VOICE_VIZ = false;

function SegmentedIconGroup({
  children,
  label,
}: {
  children: React.ReactNode;
  label: string;
}) {
  return (
    <div
      role="group"
      aria-label={label}
      className="flex items-center gap-0.5 rounded-full border border-hairline bg-canvas-soft p-0.5"
    >
      {children}
    </div>
  );
}

function SegmentedIconButton({
  selected,
  label,
  onClick,
  children,
}: {
  selected: boolean;
  label: string;
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      onClick={onClick}
      aria-label={label}
      aria-pressed={selected}
      title={label}
      className={[
        "flex h-7 w-7 items-center justify-center rounded-full transition-colors",
        selected
          ? "bg-surface-strong text-foreground shadow-sm"
          : "text-muted-soft hover:text-foreground",
      ].join(" ")}
    >
      {children}
    </button>
  );
}

export function DebugDrawer({
  assistantId,
  overlay = false,
  onClose,
  hasUnsavedChanges = false,
  onNodeActive,
  vision = false,
  dynamicVariablesEnabled = false,
  dynamicVariableDefinitions = {},
}: {
  assistantId: string | null;
  overlay?: boolean;
  onClose?: () => void;
  hasUnsavedChanges?: boolean;
  onNodeActive?: (nodeId: string | null) => void;
  vision?: boolean;
  dynamicVariablesEnabled?: boolean;
  dynamicVariableDefinitions?: Record<string, DynamicVariableDefinition>;
}) {
  const preview = useVoicePreview(assistantId, onNodeActive);
  const camera = useCameraPreview();
  const autoTest = useAutoTestRunner();
  const stopAutoTest = autoTest.stop;
  const [debugMode, setDebugMode] = useState<DebugMode>("manual");
  const [showTranscript, setShowTranscript] = useState(false);
  const [vizStyle, setVizStyle] = useState<VizStyle>("aura");
  const [view, setView] = useState<DebugView>("chat");
  const [dynamicVariableValues, setDynamicVariableValues] = useState<
    Record<string, string | number | boolean>
  >({});
  const recording =
    preview.status === "connecting" || preview.status === "connected";
  const displayedDefinitions = { ...dynamicVariableDefinitions };
  for (const [name, value] of Object.entries(preview.sessionVariables)) {
    displayedDefinitions[name] ??= {
      type:
        typeof value === "number"
          ? "number"
          : typeof value === "boolean"
            ? "boolean"
            : "string",
      required: false,
      default: null,
    };
  }
  const dynamicVariableEntries = Object.entries(displayedDefinitions);
  const resolvedDynamicVariables: Record<string, string | number | boolean> = {};
  let dynamicVariablesError = "";
  for (const [name, definition] of Object.entries(dynamicVariableDefinitions)) {
    const value = dynamicVariableValues[name] ?? definition.default;
    if (value === null || value === undefined || value === "") {
      if (definition.required && !dynamicVariablesError) {
        dynamicVariablesError = `请先填写必填变量 ${name}`;
      }
      continue;
    }
    resolvedDynamicVariables[name] = value;
  }

  const selectCamera = useCallback(
    async (deviceId: string) => {
      await camera.selectCamera(deviceId);
      preview.selectCamera(deviceId);
    },
    [camera, preview],
  );

  // 切到自动测试时停掉手动会话，避免两边抢占麦克风/对话区
  const handleModeChange = useCallback(
    (mode: DebugMode) => {
      setDebugMode(mode);
      if (mode === "auto" && recording) {
        preview.disconnect();
      }
      if (mode === "manual") {
        stopAutoTest();
      }
    },
    [preview, recording, stopAutoTest],
  );

  return (
    <aside
      className={overlay
        ? "flex h-full min-w-0 flex-1 flex-col overflow-hidden bg-card"
        : "hidden min-w-0 flex-1 flex-col overflow-hidden rounded-2xl border border-hairline bg-card shadow-sm lg:flex"}
    >
      <div className={`flex min-h-14 shrink-0 items-center justify-between gap-3 border-b border-hairline py-3 ${overlay ? "px-4" : "px-5"}`}>
        <div className="flex min-w-0 items-center gap-2.5">
          {overlay && onClose && (
            <button
              type="button"
              aria-label="关闭调试预览"
              title="关闭"
              className="flex h-8 w-8 shrink-0 items-center justify-center rounded-full border border-hairline-strong bg-card text-muted-foreground shadow-sm transition-colors hover:text-foreground"
              onClick={onClose}
            >
              <X size={16} />
            </button>
          )}
          <div className="shrink-0 text-sm font-medium text-foreground">
            调试与预览
          </div>
          {debugMode === "manual" && (
            <>
              <NetworkQualityIndicator
                quality={preview.networkQuality}
                status={preview.status}
              />
              <DebugConnectionStatus
                status={preview.status}
                micWarning={preview.micWarning}
              />
            </>
          )}
        </div>
        <div className="flex shrink-0 items-center gap-2">
          <ClientToolsPopover tools={preview.clientTools} />
          <CallPreviewLink assistantId={assistantId} />
          {dynamicVariablesEnabled && (
            <DynamicVariableValuesPopover
              entries={dynamicVariableEntries}
              values={dynamicVariableValues}
              sessionValues={preview.sessionVariables}
              readOnly={recording}
              onChange={setDynamicVariableValues}
            />
          )}
          {debugMode === "manual" && SHOW_VOICE_VIZ && view === "chat" && (
            <>
              {!showTranscript && (
                <SegmentedIconGroup label="可视化样式">
                  {VIZ_OPTIONS.map((option) => (
                    <SegmentedIconButton
                      key={option.style}
                      selected={vizStyle === option.style}
                      label={`可视化样式：${option.label}`}
                      onClick={() => setVizStyle(option.style)}
                    >
                      {option.icon}
                    </SegmentedIconButton>
                  ))}
                </SegmentedIconGroup>
              )}
              <SegmentedIconGroup label="语音视图">
                <SegmentedIconButton
                  selected={!showTranscript}
                  label="语音可视化视图"
                  onClick={() => setShowTranscript(false)}
                >
                  <Mic size={14} />
                </SegmentedIconButton>
                <SegmentedIconButton
                  selected={showTranscript}
                  label="文字聊天记录视图"
                  onClick={() => setShowTranscript(true)}
                >
                  <MessageSquareText size={14} />
                </SegmentedIconButton>
              </SegmentedIconGroup>
            </>
          )}
        </div>
      </div>
      <div className="shrink-0 space-y-2.5 border-b border-hairline px-5 py-2.5">
        <DebugModeTabs mode={debugMode} onChange={handleModeChange} />
        {debugMode === "manual" && (
          <div className="flex h-10 min-w-0 items-center rounded-[1.4rem] border border-hairline-strong bg-background px-2">
            <CameraDeviceField camera={camera} onSelect={selectCamera} />
          </div>
        )}
      </div>
      {debugMode === "auto" ? (
        <AutoTestPanel autoTest={autoTest} />
      ) : (
        <DebugVoicePanel
          view={view}
          onViewChange={setView}
          showTranscript={showTranscript}
          vizStyle={vizStyle}
          assistantId={assistantId}
          preview={preview}
          camera={camera}
          hasUnsavedChanges={hasUnsavedChanges}
          vision={vision}
          dynamicVariables={resolvedDynamicVariables}
          dynamicVariablesError={dynamicVariablesError}
        />
      )}
    </aside>
  );
}

function AutoTestPanel({
  autoTest,
}: {
  autoTest: ReturnType<typeof useAutoTestRunner>;
}) {
  const {
    state,
    selectedCase,
    selectCase,
    clearCase,
    start,
    stop,
    rerun,
  } = autoTest;

  return (
    <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
      <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
        <AutoTestLivePanel testCase={selectedCase} state={state} />
      </div>
      <div className="shrink-0 border-t border-hairline bg-card p-3">
        <AutoTestRunBar
          state={state}
          onSelectCase={selectCase}
          onClearCase={clearCase}
          onStart={start}
          onStop={stop}
          onRerun={rerun}
        />
      </div>
    </div>
  );
}

function ClientToolsPopover({ tools }: { tools: ClientToolDefinition[] }) {
  const [copiedToolName, setCopiedToolName] = useState<string | null>(null);

  const copyToolName = useCallback(async (toolName: string) => {
    await navigator.clipboard.writeText(toolName);
    setCopiedToolName(toolName);
    window.setTimeout(
      () =>
        setCopiedToolName((current) =>
          current === toolName ? null : current,
        ),
      1600,
    );
  }, []);

  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          aria-label="查看调试端支持的 Client Tools"
          title="调试端支持的 Client Tools"
          className="relative flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground"
        >
          <Wrench size={15} />
          <span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full border border-card bg-surface-strong px-1 text-[9px] tabular-nums text-foreground">
            {tools.length}
          </span>
        </button>
      </PopoverTrigger>
      <PopoverContent
        align="end"
        side="bottom"
        className="w-80 space-y-3 rounded-2xl p-4"
      >
        <div className="space-y-1">
          <div className="flex items-center gap-2 text-sm font-medium text-foreground">
            <Wrench size={15} />
            Client Tools
          </div>
          <p className="text-xs leading-5 text-muted-foreground">
            当前调试客户端已注册、能够响应的工具。
          </p>
        </div>
        {tools.length === 0 ? (
          <div className="rounded-xl border border-dashed border-hairline-strong bg-canvas-soft px-4 py-4 text-center text-xs text-muted-foreground">
            当前没有可用的 Client Tool
          </div>
        ) : (
          <div className="max-h-72 space-y-2 overflow-y-auto pr-1">
            {tools.map((tool) => (
              <div
                key={tool.name}
                className="rounded-xl border border-hairline bg-canvas-soft p-3"
              >
                <div className="flex items-center justify-between gap-3">
                  <div className="text-xs font-medium text-foreground">
                    {tool.label}
                  </div>
                  <Badge
                    variant="secondary"
                    className="shrink-0 rounded-full px-2 py-0.5 text-[10px] font-medium"
                  >
                    可用
                  </Badge>
                </div>
                <button
                  type="button"
                  onClick={() => void copyToolName(tool.name)}
                  className="mt-1 flex w-full items-center justify-between gap-2 rounded-lg px-1 py-1 text-left text-foreground transition-colors hover:bg-surface-strong"
                  aria-label={`复制工具名 ${tool.name}`}
                  title="点击复制工具名"
                >
                  <code className="min-w-0 break-all font-mono text-[11px]">
                    {tool.name}
                  </code>
                  {copiedToolName === tool.name ? (
                    <Check size={13} className="shrink-0 text-success" />
                  ) : (
                    <Copy size={13} className="shrink-0 text-muted-soft" />
                  )}
                </button>
                <p className="mt-1 text-[11px] leading-5 text-muted-foreground">
                  {tool.description}
                </p>
                <div className="mt-2 border-t border-hairline pt-2">
                  <div className="mb-1.5 text-[10px] font-medium uppercase tracking-[0.08em] text-muted-soft">
                    参数
                  </div>
                  <div className="space-y-2">
                    {tool.parameters.map((parameter) => (
                      <div key={parameter.name} className="text-[11px] leading-4">
                        <div className="flex flex-wrap items-center gap-1.5">
                          <code className="font-mono font-medium text-foreground">
                            {parameter.name}
                          </code>
                          <span className="rounded-full bg-surface-strong px-1.5 py-0.5 font-mono text-[9px] text-muted-foreground">
                            {parameter.type}
                          </span>
                          <span
                            className={
                              parameter.required
                                ? "text-destructive"
                                : "text-muted-soft"
                            }
                          >
                            {parameter.required ? "必填" : "可选"}
                          </span>
                        </div>
                        <p className="mt-0.5 text-muted-foreground">
                          {parameter.description}
                        </p>
                      </div>
                    ))}
                  </div>
                </div>
              </div>
            ))}
          </div>
        )}
      </PopoverContent>
    </Popover>
  );
}

function DynamicVariableValuesPopover({
  entries,
  values,
  sessionValues,
  readOnly,
  onChange,
}: {
  entries: [string, DynamicVariableDefinition][];
  values: Record<string, string | number | boolean>;
  sessionValues: Record<string, string | number | boolean>;
  readOnly: boolean;
  onChange: React.Dispatch<
    React.SetStateAction<Record<string, string | number | boolean>>
  >;
}) {
  function setValue(name: string, value: string | number | boolean | undefined) {
    onChange((current) => {
      const next = { ...current };
      if (value === undefined) delete next[name];
      else next[name] = value;
      return next;
    });
  }

  return (
    <Popover>
      <PopoverTrigger asChild>
        <button
          type="button"
          aria-label={readOnly ? "查看本次会话变量" : "设置本次会话变量"}
          title={readOnly ? "查看实时会话变量" : "本次会话变量"}
          className="relative flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground"
        >
          <Braces size={15} />
          <span className="absolute -right-1 -top-1 flex h-4 min-w-4 items-center justify-center rounded-full border border-card bg-surface-strong px-1 text-[9px] tabular-nums text-foreground">
            {entries.length}
          </span>
        </button>
      </PopoverTrigger>
      <PopoverContent
        align="end"
        side="bottom"
        className="w-80 space-y-3 rounded-2xl p-4"
      >
        <div className="space-y-1">
          <div className="flex items-center gap-2 text-sm font-medium text-foreground">
            <Braces size={15} />
            本次会话变量
          </div>
          <p className="text-xs leading-5 text-muted-foreground">
            {readOnly
              ? "当前值会在 Action 或工具更新变量后实时刷新。"
              : "这些值只用于下一次调试会话，不会修改助手配置。"}
          </p>
        </div>
        <div className="max-h-72 space-y-3 overflow-y-auto pr-1">
          {entries.length === 0 ? (
            <div className="rounded-xl border border-dashed border-hairline-strong bg-canvas-soft px-4 py-4 text-center">
              <div className="text-xs font-medium text-foreground">
                当前没有会话变量
              </div>
              <p className="mt-1 text-[11px] leading-5 text-muted-foreground">
                在工作流提示词、节点话术、边条件或 Action 中引用变量后，
                可在这里设置调试值。
              </p>
            </div>
          ) : entries.map(([name, definition]) => {
            const value = readOnly
              ? sessionValues[name] ?? definition.default ?? ""
              : values[name] ?? definition.default ?? "";
            return (
              <label key={name} className="block space-y-1.5">
                <span className="flex items-center gap-1.5 text-xs font-medium text-foreground">
                  <code className="font-mono">{name}</code>
                  {definition.required && (
                    <span className="text-destructive">*</span>
                  )}
                  <span className="font-normal text-muted-soft">
                    {definition.type === "string"
                      ? "文本"
                      : definition.type === "number"
                        ? "数字"
                        : "布尔值"}
                  </span>
                </span>
                {definition.type === "boolean" ? (
                  <Select
                    disabled={readOnly}
                    value={value === "" ? "unset" : String(value)}
                    onValueChange={(next) =>
                      setValue(
                        name,
                        next === "unset" ? undefined : next === "true",
                      )
                    }
                  >
                    <SelectTrigger
                      className="w-full border-hairline-strong bg-background"
                      aria-label={`会话变量 ${name}`}
                    >
                      <SelectValue />
                    </SelectTrigger>
                    <SelectContent>
                      <SelectItem value="unset">未设置</SelectItem>
                      <SelectItem value="true">True</SelectItem>
                      <SelectItem value="false">False</SelectItem>
                    </SelectContent>
                  </Select>
                ) : (
                  <Input
                    disabled={readOnly}
                    type={definition.type === "number" ? "number" : "text"}
                    value={typeof value === "boolean" ? String(value) : value}
                    onChange={(event) => {
                      const raw = event.target.value;
                      setValue(
                        name,
                        raw === ""
                          ? undefined
                          : definition.type === "number"
                            ? Number(raw)
                            : raw,
                      );
                    }}
                    aria-label={`会话变量 ${name}`}
                    placeholder={definition.required ? "必填" : "可选"}
                    className="h-9 border-hairline-strong bg-background text-xs"
                  />
                )}
              </label>
            );
          })}
        </div>
      </PopoverContent>
    </Popover>
  );
}

function CallPreviewLink({ assistantId }: { assistantId: string | null }) {
  const [callUrl, setCallUrl] = useState("");
  const [copied, setCopied] = useState(false);

  const copyLink = useCallback(async () => {
    if (!callUrl) return;
    await navigator.clipboard.writeText(callUrl);
    setCopied(true);
    window.setTimeout(() => setCopied(false), 1600);
  }, [callUrl]);

  return (
    <Popover
      onOpenChange={(open) => {
        if (open && assistantId) {
          setCallUrl(
            `${window.location.origin}/call/${encodeURIComponent(assistantId)}`,
          );
          setCopied(false);
        }
      }}
    >
      <PopoverTrigger asChild>
        <button
          type="button"
          disabled={!assistantId}
          aria-label="打开手机通话链接"
          title={assistantId ? "手机通话链接" : "请先保存助手"}
          className="flex h-8 w-8 items-center justify-center rounded-full border border-hairline bg-canvas-soft text-muted-foreground transition-colors hover:bg-surface-strong hover:text-foreground disabled:cursor-not-allowed disabled:opacity-40"
        >
          <Smartphone size={15} />
        </button>
      </PopoverTrigger>
      <PopoverContent align="end" side="bottom" className="w-80 space-y-3">
        <div className="space-y-1">
          <div className="text-sm font-medium text-foreground">手机通话预览</div>
          <p className="text-xs leading-5 text-muted-foreground">
            复制链接并在浏览器中打开，即可进入全屏视频通话界面。
          </p>
        </div>
        <div className="flex items-center gap-2">
          <Input
            readOnly
            value={callUrl}
            aria-label="手机通话链接"
            onFocus={(event) => event.currentTarget.select()}
            className="h-9 min-w-0 flex-1 text-xs"
          />
          <Button
            type="button"
            size="icon"
            variant="secondary"
            className="h-9 w-9"
            onClick={() => void copyLink()}
            aria-label="复制手机通话链接"
          >
            {copied ? <Check size={15} /> : <Copy size={15} />}
          </Button>
        </div>
        {copied && <p className="text-xs text-success">链接已复制</p>}
      </PopoverContent>
    </Popover>
  );
}

function DeviceSelectField({
  icon,
  ariaLabel,
  placeholder,
  fallbackLabel,
  value,
  devices,
  onSelect,
}: {
  icon: React.ReactNode;
  ariaLabel: string;
  placeholder: string;
  fallbackLabel: string;
  value: string;
  devices: MediaDeviceInfo[];
  onSelect: (deviceId: string) => void;
}) {
  return (
    <Select
      value={value || "default"}
      onValueChange={(nextValue) =>
        onSelect(nextValue === "default" ? "" : nextValue)
      }
    >
      <SelectTrigger
        size="sm"
        className="h-10 min-w-0 flex-1 justify-between rounded-full border-transparent bg-transparent px-3 text-sm text-foreground shadow-none hover:bg-surface-strong focus-visible:ring-0 data-[size=sm]:h-10"
        aria-label={ariaLabel}
      >
        <span className="flex min-w-0 items-center gap-2">
          {icon}
          <span className="min-w-0 truncate text-left">
            <SelectValue placeholder={placeholder} />
          </span>
        </span>
      </SelectTrigger>
      <SelectContent
        position="popper"
        side="top"
        align="start"
        className="w-[280px]"
      >
        <SelectItem value="default">{placeholder}</SelectItem>
        {devices.map((device, index) => (
          <SelectItem key={device.deviceId} value={device.deviceId}>
            {device.label || `${fallbackLabel} ${index + 1}`}
          </SelectItem>
        ))}
      </SelectContent>
    </Select>
  );
}

function MicrophoneDeviceField({ preview }: { preview: VoicePreview }) {
  return (
    <DeviceSelectField
      icon={<Mic size={15} className="shrink-0 text-muted-soft" />}
      ariaLabel="选择麦克风"
      placeholder="默认麦克风"
      fallbackLabel="麦克风"
      value={preview.selectedDeviceId}
      devices={preview.audioInputs}
      onSelect={(deviceId) => preview.selectDevice(deviceId)}
    />
  );
}

function CameraDeviceField({
  camera,
  onSelect,
}: {
  camera: CameraPreview;
  onSelect?: (deviceId: string) => void | Promise<void>;
}) {
  return (
    <DeviceSelectField
      icon={<Video size={15} className="shrink-0 text-muted-soft" />}
      ariaLabel="选择摄像头"
      placeholder="默认摄像头"
      fallbackLabel="摄像头"
      value={camera.deviceId}
      devices={camera.devices}
      onSelect={(deviceId) => void (onSelect ?? camera.selectCamera)(deviceId)}
    />
  );
}

function DebugInputModeButton({
  selected,
  label,
  onClick,
  children,
}: {
  selected: boolean;
  label: string;
  onClick: () => void;
  children: React.ReactNode;
}) {
  return (
    <button
      type="button"
      aria-label={label}
      aria-pressed={selected}
      title={label}
      onClick={onClick}
      className={[
        "flex h-8 w-8 shrink-0 items-center justify-center rounded-full transition-colors",
        selected
          ? "bg-surface-strong text-foreground"
          : "text-muted-soft hover:bg-surface-strong hover:text-foreground",
      ].join(" ")}
    >
      {children}
    </button>
  );
}

function DebugVoicePanel({
  view,
  onViewChange,
  showTranscript,
  vizStyle,
  assistantId,
  preview,
  camera,
  hasUnsavedChanges,
  vision,
  dynamicVariables,
  dynamicVariablesError,
}: {
  view: DebugView;
  onViewChange: (view: DebugView) => void;
  showTranscript: boolean;
  vizStyle: VizStyle;
  assistantId: string | null;
  preview: VoicePreview;
  camera: CameraPreview;
  hasUnsavedChanges: boolean;
  vision: boolean;
  dynamicVariables: Record<string, string | number | boolean>;
  dynamicVariablesError: string;
}) {
  const photoCapture = usePhotoCaptureTool(preview, vision);
  const {
    status,
    error,
    micWarning,
    localStream,
    remoteStream,
    messages,
    sendText,
    sendUserInput,
    appendUserImage,
    removeUserImage,
    connect,
    disconnect,
    audioRef,
  } = preview;
  const recording = status === "connecting" || status === "connected";
  const [textDraft, setTextDraft] = useState("");
  const [inputMode, setInputMode] = useState<DebugInputMode>("mic");
  const [pendingImage, setPendingImage] =
    useState<PendingDebugImage | null>(null);
  const [inputError, setInputError] = useState("");
  const [sendingInput, setSendingInput] = useState(false);
  const [draggingImage, setDraggingImage] = useState(false);
  const [clientDialogContainer, setClientDialogContainer] =
    useState<HTMLDivElement | null>(null);
  const [messageDialogOpen, setMessageDialogOpen] = useState(false);
  const inChatView = view === "chat" && (!SHOW_VOICE_VIZ || showTranscript);
  const idleOrFailed = status === "idle" || status === "failed";
  const showIdleHub =
    idleOrFailed &&
    (view === "video" || (messages.length === 0 && inChatView));
  const startBlockedMessage = !assistantId
    ? "请先保存助手，再开始对话。"
    : hasUnsavedChanges
      ? "请先保存当前改动，再开始对话。"
      : dynamicVariablesError
        ? dynamicVariablesError
      : "";
  const startDisabled = status === "connecting" || Boolean(startBlockedMessage);

  useEffect(() => {
    return () => {
      if (pendingImage) URL.revokeObjectURL(pendingImage.previewUrl);
    };
  }, [pendingImage]);

  function stageImage(file: File) {
    setInputError("");
    if (!vision) {
      setInputError("请先为助手开启视觉理解，再添加图片。");
      return;
    }
    if (!file.type.startsWith("image/")) {
      setInputError("只能添加图片文件。");
      return;
    }
    if (file.size > DEBUG_IMAGE_MAX_BYTES) {
      setInputError("图片不能超过 10 MB。");
      return;
    }
    setPendingImage({ file, previewUrl: URL.createObjectURL(file) });
    setInputMode("text");
  }

  async function handleSendInput() {
    const text = textDraft.trim();
    if (!pendingImage) {
      if (sendText(text)) {
        setTextDraft("");
        setInputError("");
      }
      return;
    }
    if (status !== "connected" || sendingInput) return;

    setSendingInput(true);
    setInputError("");
    let assetToken = "";
    let inputId = "";
    try {
      const imageUrl = await fileToDataUrl(pendingImage.file);
      const asset = await inputAssetsApi.uploadImage(pendingImage.file);
      assetToken = asset.assetToken;
      const parts: UserInputPart[] = [];
      if (text) parts.push({ type: "input_text", text });
      parts.push({
        type: "input_image",
        source: { type: "uploaded_asset", asset_token: assetToken },
      });

      const timestamp = new Date().toISOString();
      inputId = createUserInputId();
      appendUserImage(inputId, imageUrl, timestamp, text);
      await sendUserInput(parts, { inputId });
      setTextDraft("");
      setPendingImage(null);
    } catch (sendError) {
      if (inputId) removeUserImage(inputId);
      if (assetToken) {
        void inputAssetsApi.remove(assetToken).catch(() => {});
      }
      setInputError(
        sendError instanceof Error ? sendError.message : "图片发送失败，请重试。",
      );
    } finally {
      setSendingInput(false);
    }
  }

  function handlePaste(event: React.ClipboardEvent<HTMLTextAreaElement>) {
    const image = Array.from(event.clipboardData.items)
      .find((item) => item.type.startsWith("image/"))
      ?.getAsFile();
    if (!image) return;
    event.preventDefault();
    stageImage(image);
  }

  function handleDragOver(event: React.DragEvent<HTMLDivElement>) {
    if (!hasDraggedImage(event.dataTransfer)) return;
    event.preventDefault();
    event.dataTransfer.dropEffect = "copy";
    setDraggingImage(true);
  }

  function handleDragLeave(event: React.DragEvent<HTMLDivElement>) {
    const nextTarget = event.relatedTarget;
    if (nextTarget instanceof Node && event.currentTarget.contains(nextTarget)) {
      return;
    }
    setDraggingImage(false);
  }

  function handleDrop(event: React.DragEvent<HTMLDivElement>) {
    if (!hasDraggedImage(event.dataTransfer)) return;
    event.preventDefault();
    setDraggingImage(false);
    const image = Array.from(event.dataTransfer.files).find((file) =>
      file.type.startsWith("image/"),
    );
    if (image) stageImage(image);
  }

  function removePendingImage() {
    if (!sendingInput) {
      setPendingImage(null);
      setInputError("");
    }
  }

  const startConversation = useCallback(async () => {
    if (!assistantId || hasUnsavedChanges) return;
    if (dynamicVariablesError) return;
    await connect({
      visionEnabled: vision,
      dynamicVariables,
    });
  }, [
    assistantId,
    connect,
    dynamicVariables,
    dynamicVariablesError,
    hasUnsavedChanges,
    vision,
  ]);

  return (
    <div className="flex min-h-0 flex-1 flex-col overflow-hidden">
      {/* 后端 TTS 音频经 WebRTC 媒体流过来,挂这里播放 */}
      <audio ref={audioRef} autoPlay playsInline className="hidden" />
      <div
        ref={setClientDialogContainer}
        className="relative isolate flex min-h-0 flex-1 flex-col overflow-hidden"
      >
        <ClientMessageDialog
          preview={preview}
          container={clientDialogContainer}
          contained
          onOpenChange={setMessageDialogOpen}
        />
        <div
          className={[
            "flex min-h-0 flex-1 flex-col overflow-hidden transition-[filter] duration-200",
            messageDialogOpen
              ? "pointer-events-none blur-md brightness-[0.92]"
              : "",
          ].join(" ")}
        >
      {vision && !showIdleHub ? (
        <DebugVisionWorkspace
          view={view}
          onViewChange={onViewChange}
          messages={messages}
          recording={recording}
          camera={camera}
          videoStream={preview.videoStream}
          photoCaptureVisible={photoCapture.visible}
          photoCapturing={photoCapture.capturing}
          photoError={photoCapture.error}
          onPhotoCapture={photoCapture.capture}
        />
      ) : view === "video" ? (
        showIdleHub ? (
          <DebugIdleHub
            status={status}
            error={error}
            message={startBlockedMessage}
            connect={startConversation}
          />
        ) : (
          <DebugVideoPanel camera={camera} streamOverride={preview.videoStream} />
        )
      ) : !SHOW_VOICE_VIZ || showTranscript ? (
        showIdleHub ? (
          <DebugIdleHub
            status={status}
            error={error}
            message={startBlockedMessage}
            connect={startConversation}
          />
        ) : (
          <DebugTranscriptPanel messages={messages} recording={recording} />
        )
      ) : (
        <div className="scrollbar-subtle relative flex min-h-0 flex-1 flex-col items-center justify-center gap-3 overflow-y-auto px-6 py-3 text-center">
          <div
            className="pointer-events-none absolute left-1/2 top-2 h-72 w-72 -translate-x-1/2 rounded-full opacity-50 blur-3xl"
            style={{
              background:
                "radial-gradient(circle, color-mix(in srgb, var(--gradient-sky) 42%, transparent), color-mix(in srgb, var(--gradient-lavender) 18%, transparent) 48%, transparent 74%)",
            }}
          />

          <Badge
            variant="secondary"
            className="relative gap-1.5 rounded-full border border-hairline bg-canvas-soft px-3 py-1 text-[11px] font-medium text-muted-foreground shadow-none"
          >
            <span
              className={[
                "h-1.5 w-1.5 rounded-full",
                recording ? "animate-pulse bg-success" : "bg-muted-soft",
              ].join(" ")}
            />
            {recording ? "会话进行中" : "准备开始"}
          </Badge>

          <div className="relative flex h-[200px] w-[240px] shrink-0 items-center justify-center">
            {(() => {
              const shared = {
                active: Boolean(localStream),
                stream: localStream,
                className: "relative shrink-0",
              } as const;
              if (vizStyle === "aura")
                return <AuraVisualizer {...shared} size={200} />;
              if (vizStyle === "nebula")
                return <NebulaVisualizer {...shared} size={200} />;
              if (vizStyle === "bars")
                return <SpectrumVisualizer {...shared} size={200} />;
              return <WaveVisualizer {...shared} size={200} />;
            })()}
          </div>

          <div className="relative max-w-xs space-y-1.5">
            <div className="font-display display-sm text-foreground">
              {status === "connecting"
                ? "连接中…"
                : status === "connected"
                  ? micWarning
                    ? "仅收听模式"
                    : "我在聆听"
                  : "开始一次语音对话"}
            </div>
            <p className="mx-auto text-xs leading-5 text-muted-foreground">
              {status === "failed"
                  ? error ||
                    "连接失败，请确认后端已启动且助手已保存后重试。"
                  : !assistantId
                    ? "请先保存助手，再开始语音预览。"
                    : hasUnsavedChanges
                      ? "请先保存当前改动，再开始语音预览。"
                    : micWarning
                      ? `${micWarning} 可接收助手播报，但无法发送语音。`
                    : recording
                      ? "直接说话即可。助手会在您停顿后自然回应。"
                      : "测试语音识别、响应速度与助手的播报效果。"}
            </p>
          </div>

          <Button
            disabled={recording ? status === "connecting" : startDisabled}
            onClick={() => {
              if (recording) {
                disconnect();
              } else {
                void startConversation();
              }
            }}
            className={[
              "relative h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm transition-transform hover:scale-[1.03]",
              recording
                ? "bg-destructive text-white hover:bg-destructive/90"
                : "",
            ].join(" ")}
            aria-label={recording ? "结束语音测试" : "开始语音测试"}
          >
            {status === "connecting" ? (
              <Loader2 size={18} className="animate-spin" />
            ) : recording ? (
              <PhoneOff size={18} />
            ) : (
              <Mic size={18} />
            )}
            {recording ? "结束对话" : "开始对话"}
          </Button>
        </div>
      )}

        </div>
      </div>

      <div className="shrink-0 border-t border-hairline bg-card p-3">
        <div className="flex items-end gap-2">
          <div className="min-w-0 flex-1">
            <div
              onDragOver={handleDragOver}
              onDragLeave={handleDragLeave}
              onDrop={handleDrop}
              className={[
                "relative flex min-h-10 min-w-0 items-end gap-1 overflow-hidden rounded-[1.4rem] border bg-background px-2 transition-colors",
                draggingImage
                  ? "border-foreground"
                  : "border-hairline-strong",
              ].join(" ")}
            >
              <div className="mb-1 flex shrink-0 items-center gap-0.5 rounded-full bg-canvas-soft p-0.5">
                <DebugInputModeButton
                  selected={inputMode === "mic"}
                  label="选择麦克风设备"
                  onClick={() => setInputMode("mic")}
                >
                  <Mic size={15} />
                </DebugInputModeButton>
                <DebugInputModeButton
                  selected={inputMode === "text"}
                  label="文字输入"
                  onClick={() => setInputMode("text")}
                >
                  <MessageSquareText size={15} />
                </DebugInputModeButton>
              </div>
              {inputMode === "mic" ? (
                <div className="flex min-w-0 flex-1">
                  <MicrophoneDeviceField preview={preview} />
                </div>
              ) : (
                <div className="min-w-0 flex-1 py-1">
                  {pendingImage && (
                    <div className="flex items-center gap-2 px-2 pb-1 pt-0.5">
                      <div className="group relative h-14 w-14 shrink-0 overflow-hidden rounded-xl border border-hairline bg-canvas-soft">
                        {/* eslint-disable-next-line @next/next/no-img-element */}
                        <img
                          src={pendingImage.previewUrl}
                          alt="待发送图片预览"
                          className="h-full w-full object-cover"
                        />
                        <button
                          type="button"
                          aria-label="移除待发送图片"
                          title="移除图片"
                          disabled={sendingInput}
                          onClick={removePendingImage}
                          className="absolute right-1 top-1 flex h-5 w-5 items-center justify-center rounded-full bg-background/90 text-foreground shadow-sm transition-colors hover:bg-background disabled:cursor-not-allowed"
                        >
                          <X size={12} />
                        </button>
                        {sendingInput && (
                          <span className="absolute inset-0 flex items-center justify-center bg-background/65">
                            <Loader2 size={18} className="animate-spin" />
                          </span>
                        )}
                      </div>
                      <div className="min-w-0">
                        <p className="truncate text-xs font-medium text-foreground">
                          {pendingImage.file.name || "粘贴的图片"}
                        </p>
                        <p className="mt-0.5 text-[11px] text-muted-soft">
                          按 Enter 上传并发送
                        </p>
                      </div>
                    </div>
                  )}
                  <Textarea
                    rows={1}
                    value={textDraft}
                    disabled={status !== "connected" || sendingInput}
                    onChange={(event) => {
                      setTextDraft(event.target.value);
                      if (inputError) setInputError("");
                    }}
                    onPaste={handlePaste}
                    onKeyDown={(event) => {
                      if (
                        event.key === "Enter" &&
                        !event.shiftKey &&
                        !event.nativeEvent.isComposing
                      ) {
                        event.preventDefault();
                        void handleSendInput();
                      }
                    }}
                    placeholder={
                      status === "connected"
                        ? vision
                          ? "输入文字，或粘贴 / 拖入图片…"
                          : "输入文字发送给助手，将打断当前播报…"
                        : "开始对话后可输入文字…"
                    }
                    className="max-h-24 min-h-8 resize-none overflow-y-auto border-transparent bg-transparent px-2 py-1 text-sm leading-6 text-foreground shadow-none outline-none placeholder:text-muted-soft focus-visible:ring-0 disabled:opacity-100"
                  />
                </div>
              )}
              {draggingImage && (
                <div className="pointer-events-none absolute inset-0 z-20 flex items-center justify-center gap-2 rounded-[1.4rem] bg-background/95 text-xs font-medium text-foreground">
                  <ImageIcon size={16} />
                  松开以添加图片
                </div>
              )}
            </div>
            {inputError && (
              <p className="px-2 pt-1 text-[11px] leading-4 text-destructive">
                {inputError}
              </p>
            )}
          </div>
          {inputMode === "text" && (
            <Button
              size="icon"
              className="h-10 w-10 shrink-0 rounded-full"
              aria-label={sendingInput ? "正在发送图片" : "发送调试消息"}
              disabled={
                status !== "connected" ||
                sendingInput ||
                (!textDraft.trim() && !pendingImage)
              }
              onClick={() => void handleSendInput()}
            >
              {sendingInput ? (
                <Loader2 size={16} className="animate-spin" />
              ) : (
                <Send size={16} />
              )}
            </Button>
          )}
          {!showIdleHub && (
            <div className="flex shrink-0 flex-col items-end gap-1">
              <Button
                size="sm"
                disabled={recording ? status === "connecting" : startDisabled}
                onClick={() => {
                  if (recording) {
                    disconnect();
                  } else {
                    void startConversation();
                  }
                }}
                className={[
                  "h-10 gap-1.5 rounded-full px-4",
                  recording
                    ? "bg-destructive text-white hover:bg-destructive/90"
                    : "",
                ].join(" ")}
                aria-label={recording ? "结束语音测试" : "开始语音测试"}
              >
                {status === "connecting" ? (
                  <Loader2 size={14} className="animate-spin" />
                ) : recording ? (
                  <PhoneOff size={14} />
                ) : (
                  <Mic size={14} />
                )}
                {recording ? "结束对话" : "开始对话"}
              </Button>
              {!recording && startBlockedMessage && (
                <span className="max-w-40 text-right text-[11px] leading-4 text-destructive">
                  {startBlockedMessage}
                </span>
              )}
            </div>
          )}
        </div>
      </div>

      {/* 底部双轨波形默认收起；展开状态只由用户操作控制。 */}
      <WaveformTimelinePanel
        defaultOpen={false}
        userStream={localStream}
        agentStream={remoteStream}
        active={status === "connected"}
      />
    </div>
  );
}

// 空闲态只保留一个明确入口,避免中间区域显得拥挤。
function DebugIdleHub({
  status,
  error,
  message,
  connect,
}: {
  status: VoicePreviewStatus;
  error: string | null;
  message: string;
  connect: () => Promise<void>;
}) {
  const helperText =
    message ||
    (status === "failed"
      ? error || "连接失败，请确认后端已启动且助手已保存后重试。"
      : "");

  return (
    <div className="flex min-h-0 flex-1 flex-col items-center justify-center gap-3 px-6 py-8 text-center">
      <div className="relative inline-flex">
        {!message && (
          <>
            <span
              aria-hidden
              className="mobile-call-ripple pointer-events-none absolute inset-0 rounded-full bg-primary/35"
            />
            <span
              aria-hidden
              className="mobile-call-ripple mobile-call-ripple-delay pointer-events-none absolute inset-0 rounded-full bg-primary/25"
            />
          </>
        )}
        <Button
          disabled={Boolean(message)}
          onClick={() => void connect()}
          className="relative z-10 h-11 gap-2 rounded-full px-6 text-sm font-medium shadow-sm"
          aria-label={status === "failed" ? "重新连接" : "开始语音测试"}
        >
          <Mic size={18} />
          {status === "failed" ? "重新连接" : "开始对话"}
        </Button>
      </div>
      {helperText && (
        <p
          className={[
            "max-w-xs text-xs leading-5",
            message ? "text-destructive" : "text-muted-foreground",
          ].join(" ")}
        >
          {helperText}
        </p>
      )}
    </div>
  );
}

function DebugVisionWorkspace({
  view,
  onViewChange,
  messages,
  recording,
  camera,
  videoStream,
  photoCaptureVisible,
  photoCapturing,
  photoError,
  onPhotoCapture,
}: {
  view: DebugView;
  onViewChange: (view: DebugView) => void;
  messages: ChatMessage[];
  recording: boolean;
  camera: CameraPreview;
  videoStream: MediaStream | null;
  photoCaptureVisible: boolean;
  photoCapturing: boolean;
  photoError: string | null;
  onPhotoCapture: () => Promise<void>;
}) {
  const containerRef = useRef<HTMLDivElement>(null);
  const [position, setPosition] = useState({ x: 16, y: 16 });
  const positionedRef = useRef(false);
  const dragRef = useRef<{
    pointerId: number;
    startX: number;
    startY: number;
    originX: number;
    originY: number;
    moved: boolean;
  } | null>(null);
  const latestMessage = [...messages]
    .reverse()
    .find(
      (message) =>
        message.content.trim() || (message.attachments?.length ?? 0) > 0,
    );

  useEffect(() => {
    const container = containerRef.current;
    if (!container) return;
    const rect = container.getBoundingClientRect();
    if (!positionedRef.current) {
      positionedRef.current = true;
      setPosition({ x: Math.max(16, rect.width - 192), y: 16 });
      return;
    }
    setPosition((current) => ({
      x: Math.min(Math.max(16, current.x), Math.max(16, rect.width - 192)),
      y: Math.min(Math.max(16, current.y), Math.max(16, rect.height - 116)),
    }));
  }, [view]);

  const moveFloatingVideo = useCallback((event: React.PointerEvent<HTMLButtonElement>) => {
    const drag = dragRef.current;
    const container = containerRef.current;
    if (!drag || !container || drag.pointerId !== event.pointerId) return;
    const dx = event.clientX - drag.startX;
    const dy = event.clientY - drag.startY;
    if (Math.abs(dx) + Math.abs(dy) > 4) drag.moved = true;
    const rect = container.getBoundingClientRect();
    setPosition({
      x: Math.min(Math.max(16, drag.originX + dx), Math.max(16, rect.width - 192)),
      y: Math.min(Math.max(16, drag.originY + dy), Math.max(16, rect.height - 116)),
    });
  }, []);

  if (view === "video") {
    return (
      <div ref={containerRef} className="relative flex min-h-0 flex-1 overflow-hidden bg-black">
        <DebugVideoPanel camera={camera} streamOverride={videoStream} />
        <button
          type="button"
          onClick={() => onViewChange("chat")}
          className="absolute left-4 right-4 top-4 z-10 flex h-16 items-center gap-3 overflow-hidden rounded-2xl border border-white/15 bg-[#07101a]/55 px-4 py-2 text-left text-white shadow-lg backdrop-blur-md transition-colors hover:bg-[#07101a]/70 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-white/40"
          aria-label="切换到聊天记录"
        >
          <MessageSquareText size={17} className="shrink-0 text-white/70" />
          <span className="line-clamp-2 min-w-0 flex-1 text-xs leading-5 text-white/90">
            <span className="mr-1.5 font-medium text-white/55">
              {latestMessage?.role === "user" ? "我：" : "助手："}
            </span>
            <span>
              {latestMessage?.content ||
                (latestMessage?.attachments?.length
                  ? "发送了一张照片"
                  : "暂无消息，点击返回聊天记录")}
            </span>
          </span>
        </button>
        {photoCaptureVisible && (
          <DebugPhotoCaptureButton
            capturing={photoCapturing}
            enabled={Boolean(videoStream)}
            error={photoError}
            onCapture={onPhotoCapture}
          />
        )}
      </div>
    );
  }

  return (
    <div ref={containerRef} className="relative flex min-h-0 flex-1 overflow-hidden">
      <DebugTranscriptPanel messages={messages} recording={recording} />
      <button
        type="button"
        style={{ left: position.x, top: position.y }}
        className="absolute z-10 h-[100px] w-44 touch-none overflow-hidden rounded-2xl border border-white/20 bg-black text-left shadow-xl ring-1 ring-black/10 transition-shadow hover:shadow-2xl focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-primary"
        aria-label="拖动摄像头浮窗，点击切换到视频流"
        onPointerDown={(event) => {
          event.currentTarget.setPointerCapture(event.pointerId);
          dragRef.current = {
            pointerId: event.pointerId,
            startX: event.clientX,
            startY: event.clientY,
            originX: position.x,
            originY: position.y,
            moved: false,
          };
        }}
        onPointerMove={moveFloatingVideo}
        onPointerUp={(event) => {
          const drag = dragRef.current;
          event.currentTarget.releasePointerCapture(event.pointerId);
          dragRef.current = null;
          if (drag && !drag.moved) onViewChange("video");
        }}
        onPointerCancel={() => {
          dragRef.current = null;
        }}
      >
        <DebugVideoPanel camera={camera} streamOverride={videoStream} compact />
      </button>
      {photoCaptureVisible && (
        <DebugPhotoCaptureButton
          capturing={photoCapturing}
          enabled={Boolean(videoStream)}
          error={photoError}
          onCapture={onPhotoCapture}
        />
      )}
    </div>
  );
}

function DebugPhotoCaptureButton({
  capturing,
  enabled,
  error,
  onCapture,
}: {
  capturing: boolean;
  enabled: boolean;
  error: string | null;
  onCapture: () => Promise<void>;
}) {
  return (
    <div className="absolute bottom-4 left-1/2 z-20 flex -translate-x-1/2 flex-col items-center gap-2">
      {error && (
        <div className="max-w-xs rounded-full border border-destructive/20 bg-background/90 px-3 py-1 text-xs text-destructive shadow-sm backdrop-blur">
          {error}
        </div>
      )}
      <Button
        type="button"
        size="icon"
        onClick={() => void onCapture()}
        disabled={capturing || !enabled}
        aria-label={capturing ? "正在提交照片" : "拍照并发送"}
        title={capturing ? "正在提交照片" : "拍照并发送"}
        className="size-12 rounded-full shadow-lg"
      >
        {capturing ? (
          <Loader2 className="size-5 animate-spin" />
        ) : (
          <ScanLine className="size-5" />
        )}
      </Button>
    </div>
  );
}

function DebugVideoPanel({
  camera,
  streamOverride,
  compact = false,
}: {
  camera: CameraPreview;
  streamOverride?: MediaStream | null;
  compact?: boolean;
}) {
  const { stream, error, starting, active } = camera;
  const effectiveStream = streamOverride ?? stream;
  const effectiveActive = Boolean(effectiveStream) || active;
  const videoRef = useRef<HTMLVideoElement>(null);

  useEffect(() => {
    if (videoRef.current) videoRef.current.srcObject = effectiveStream;
  }, [effectiveStream]);

  return (
    <div
      className={[
        "relative flex min-h-0 flex-1 items-center justify-center overflow-hidden",
        compact ? "h-full w-full" : "",
        effectiveActive ? "bg-black" : "bg-canvas-soft",
      ].join(" ")}
    >
      <video
        ref={videoRef}
        autoPlay
        playsInline
        muted
        className={[
          "h-full w-full -scale-x-100",
          compact ? "object-cover" : "object-contain",
          effectiveActive ? "" : "hidden",
        ].join(" ")}
      />
      {!effectiveActive ? (
        <div className="flex max-w-xs flex-col items-center gap-2 px-6 text-center">
          <Video size={28} className="text-muted-soft" />
          <div className="text-sm font-medium text-foreground">
            {starting ? "正在开启摄像头…" : "摄像头不可用"}
          </div>
          {error && (
            <p className="text-xs leading-5 text-muted-foreground">{error}</p>
          )}
        </div>
      ) : null}
    </div>
  );
}

function DebugConnectionStatus({
  status,
  micWarning,
}: {
  status: VoicePreviewStatus;
  micWarning: string | null;
}) {
  const recording = status === "connecting" || status === "connected";
  const label =
    status === "connecting"
      ? "连接中…"
      : status === "connected"
        ? micWarning
          ? "仅收听"
          : "进行中"
        : status === "failed"
          ? "连接失败"
          : "准备开始";

  return (
    <span className="flex min-w-0 items-center gap-1.5 truncate text-xs text-muted-foreground">
      <span
        className={[
          "h-1.5 w-1.5 shrink-0 rounded-full",
          recording
            ? "animate-pulse bg-success"
            : status === "failed"
              ? "bg-destructive"
              : "bg-muted-soft",
        ].join(" ")}
      />
      {label}
    </span>
  );
}

// ISO 时间戳 → HH:MM(本地时区),解析失败返回空串
function formatMessageTime(iso: string): string {
  const d = new Date(iso);
  if (Number.isNaN(d.getTime())) return "";
  const pad = (n: number) => String(n).padStart(2, "0");
  return `${pad(d.getHours())}:${pad(d.getMinutes())}`;
}

function DebugTranscriptPanel({
  messages,
  recording = false,
}: {
  messages: ChatMessage[];
  recording?: boolean;
}) {
  const scrollRef = useRef<HTMLDivElement>(null);
  const contentRef = useRef<HTMLDivElement>(null);

  const scrollToBottom = useCallback(() => {
    const el = scrollRef.current;
    if (el) el.scrollTop = el.scrollHeight;
  }, []);

  // Scroll when message list changes (before paint).
  useLayoutEffect(() => {
    scrollToBottom();
  }, [messages, scrollToBottom]);

  // Images load after the message row mounts; re-scroll when content height grows.
  useEffect(() => {
    const content = contentRef.current;
    if (!content) return;
    const observer = new ResizeObserver(() => scrollToBottom());
    observer.observe(content);
    return () => observer.disconnect();
  }, [scrollToBottom]);

  if (messages.length === 0) {
    return (
      <div
        className={[
          "scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-3",
          recording ? "items-center justify-center" : "pt-5",
        ].join(" ")}
      >
        <div className="text-center">
          <MessageSquareText size={22} className="mx-auto text-muted-soft" />
          <div className="mt-2 text-sm font-medium text-foreground">
            {recording ? "暂无聊天记录" : "尚未开始对话"}
          </div>
          <p className="mx-auto mt-1 max-w-xs text-xs leading-5 text-muted-foreground">
            {recording
              ? "开口说话或在下方输入文字，对话内容会实时显示在这里。"
              : "点击「开始对话」后，语音与文字消息会实时显示在这里。"}
          </p>
        </div>
      </div>
    );
  }

  return (
    <div
      ref={scrollRef}
      className="scrollbar-subtle flex min-h-0 flex-1 flex-col overflow-y-auto px-5 py-4"
    >
      <div ref={contentRef} className="flex flex-col gap-4">
        {messages.map((message) => {
          const time = formatMessageTime(message.timestamp);
          return message.role === "assistant" ? (
            <div
              key={message.id}
              className="flex max-w-[88%] flex-col gap-1 self-start"
            >
              <span className="px-1 text-[11px] text-muted-soft">
                助手{time ? ` · ${time}` : ""}
              </span>
              <div className="whitespace-pre-wrap rounded-2xl rounded-tl-sm bg-surface-strong px-4 py-2.5 text-sm leading-6 text-foreground">
                {message.content || (message.streaming ? "…" : "")}
              </div>
            </div>
          ) : (
            <div
              key={message.id}
              className="flex max-w-[88%] flex-col items-end gap-1 self-end"
            >
              <span className="px-1 text-[11px] text-muted-soft">
                我{time ? ` · ${time}` : ""}
              </span>
              <div
                className={[
                  "overflow-hidden whitespace-pre-wrap rounded-2xl rounded-tr-sm bg-primary text-sm leading-6 text-primary-foreground",
                  message.attachments?.length ? "p-1" : "px-4 py-2.5",
                ].join(" ")}
              >
                {message.attachments?.map((attachment) => (
                  // eslint-disable-next-line @next/next/no-img-element
                  <img
                    key={attachment.id}
                    src={attachment.url}
                    alt={attachment.alt}
                    className="max-h-72 w-full rounded-[0.8rem] object-cover"
                    onLoad={scrollToBottom}
                  />
                ))}
                {message.content && (
                  <div className={message.attachments?.length ? "px-3 py-2" : ""}>
                    {message.content}
                  </div>
                )}
              </div>
            </div>
          );
        })}
      </div>
    </div>
  );
}
