import {
  AppstoreOutlined,
  ArrowDownOutlined,
  ArrowUpOutlined,
  ClockCircleOutlined,
  CloseOutlined,
  CopyOutlined,
  DeleteOutlined,
  LoadingOutlined,
  MessageOutlined,
  PictureOutlined,
  PlusOutlined,
  ReloadOutlined,
  SendOutlined,
  SettingOutlined,
  StopOutlined,
  VideoCameraOutlined,
} from "@ant-design/icons";
import {
  useCallback,
  useEffect,
  useMemo,
  useRef,
  useState,
  type CSSProperties,
  type ChangeEvent,
  type KeyboardEvent,
  type ReactNode,
  type SyntheticEvent,
} from "react";
import "../../styles/pages/workbench.css";
import type { WebUserSession } from "../../types";
import { aiGenerationClient } from "../../api/aiGenerationClient";
import { claimGenerationSlot, getActiveGenerationTaskCount, getEffectiveGenerationLimit, getGenerationUserKey, releaseGenerationSlot } from "../../api/generationConcurrency";
import { preUploadReference } from "../../api/referenceUploadService";
import { assetClient } from "../../api/assetClient";
import { communityClient } from "../../api/communityClient";
import { loadRechargeModal, type RechargeModalComponent } from "../../components/RechargeModal/loadRechargeModal";
import { useGenerationTasks } from "../../hooks/useGenerationTasks";

import { conversationClient, type ConversationSummary } from "../../api/conversationClient";
import { modelCapabilitiesClient } from "../../api/modelCapabilitiesClient";
import { publicPricingClient, type PublicModelPrice } from "../../api/publicPricingClient";
import type { CreatePreviewTaskInput } from "../../api/webGenerationGateway";
import type { WebProjectSummary } from "../../types";
import {
  communityCaseToPromptCase,
  getPromptCaseCardClassName,
  type PromptCaseViewModel,
} from "../community/communityCaseUtils";
import ProjectSidebar from "./ProjectSidebar";
import {
  ChatAttachmentPreview,
  GenerationPendingCard,
  ImmersiveVideoPlayer,
  MarkdownMessage,
  ResultCard,
} from "./components/WorkbenchChatCards";
import { translateTaskError } from "../../utils/translateTaskError";
import {
  buildLocalTimeoutMessage,
  FALLBACK_TEXT_TOKEN_CREDIT_RATE,
  formatTextTokenCreditRule,
  getTaskTimeoutPolicy,
  isTaskLocallyTimedOut,
} from "../../utils/taskLifecycle";
import { detectMentionTrigger } from "../../utils/mentionTrigger";
import {
  isHappyHorseModel,
  toHappyHorseDisplayModel,
} from "../../utils/happyHorseRouting";
import { isViduModel } from "../../utils/viduRouting";
import { isPixverseModel } from "../../utils/pixverseRouting";
import { resolveVideoRequestModel } from "../../utils/resolveVideoModel";
import {
  calculateEnterpriseVideoCredits,
  ENTERPRISE_DEFAULT_VIDEO_MODEL,
  type EnterpriseVideoPricingConfig,
} from "../../utils/enterpriseVideoPolicy";
import { resolveTextTokenCreditRate } from "../../utils/modelPricing";
import {
  getImageQualityOptionsForContext,
  getDefaultImageQuality,
  getDefaultImageQualityForContext,
  getVideoQualityOptions,
  getDefaultVideoQuality,
  getVideoQualityLabel,
} from "../../utils/modelOptions";
import { filterImageModelOptionsForSession } from "../../utils/imageModelVisibility";
import { persistWorkbenchResultAsset, type PersistedWorkbenchResultAsset } from "./workbenchResultPersistence";
import {
  type WorkbenchMode,
  type ToolbarMenuId,
  type ReferenceKind,
  type WorkbenchOption,
  type WorkbenchFieldGroup,
  type ReferenceItem,
  type ChatAttachment,
  type ChatMessage,
  type DeleteDialogState,
  type WorkbenchKeepaliveTask,
  MODE_META,
  MODE_OPTIONS,
  CHAT_MODEL_OPTIONS,
  THINKING_SPEED_OPTIONS,
  THINKING_DEPTH_OPTIONS,
  IMAGE_MODEL_OPTIONS,
  VIDEO_MODEL_OPTIONS,
  RATIO_OPTIONS,
  GRID_MODE_OPTIONS,
  GRID_SUPPORTED_MODELS,
  VIDEO_FRAME_OPTIONS,
  VIDEO_DURATION_OPTIONS,
  WORKBENCH_TASK_MAX_POLL_FAILURES,
  CHAT_NATURAL_SYSTEM_PROMPT,
  CHAT_TURN_STYLE_REMINDER,
  getCachedRole,
  createId,
  formatWorkbenchTimestamp,
  buildChatAttachments,
  buildNaturalChatHistoryMessages,
  getErrorText,
  isAuthFailure,
  isInsufficientBalance,
  isInsufficientBalanceMessage,
  getPersistableMessages,
  shouldPersistPatch,
  buildAssistantResult,
} from "./workbenchConstants";
import {
  readStoredMessages,
  readStoredPromptHistory,
  readStoredActiveConversationId,
  persistActiveConversationId,
  persistMessages,
  clearWorkbenchLocalState,
  persistPromptHistory,
  readStoredKeepaliveTasks,
  persistKeepaliveTasks,
} from "./workbenchStorage";
import {
  getReferenceAccept,
  getReferenceUploadLabel,
  getReferenceLimit,
  getReferenceKindLabel,
  hexToRgbTriplet,
  inferReferenceKind,
  disposeReferencePreview,
  buildReferenceFingerprint,
  compressReferenceImageIfNeeded,
  buildReferenceToken,
  resolveReferenceUrls,
} from "./workbenchReferenceUtils";
import {
  renderPromptPreviewNodes,
  getPromptMentionTokenRanges,
  removePromptMentionTokenFromText,
  removePromptTextRange,
} from "./workbenchMentionUtils";
import {
  findPromptMentionRangeInside,
  findPromptMentionRangeOverlap,
  ReferencePreview,
  PromptPreviewLayer,
} from "./WorkbenchPromptPreview";
import { SelectChip, CompoundSelectChip, InlineOptionChip } from "./WorkbenchSelectChips";
import OnboardingTour, { type TourPhaseId } from "../../components/OnboardingTour";

export type { WorkbenchResultActionPayload } from "./workbenchConstants";

interface WorkbenchPageProps {
  isAuthenticated: boolean;
  session: WebUserSession | null;
  onboarding?: boolean;
  onEndOnboarding?: () => void;
  onRequireLogin: (input: CreatePreviewTaskInput) => void;
  onOpenResultInCanvas?: (payload: import("./workbenchConstants").WorkbenchResultActionPayload) => void;
  onRefreshUsage?: () => void;
  resetToken?: number;
}

// ─── Component ───────────────────────────────────────────────────────────

// (All types, constants, helpers, and sub-components extracted to sibling modules)

// ─── REDUNDANT LOCAL DEFINITIONS REMOVED ─────────────────────────────
// The following block (originally ~1200 lines of types, constants, utility
// functions, and sub-components) has been extracted to sibling modules:
//   workbenchConstants.ts, workbenchStorage.ts, workbenchReferenceUtils.ts,
//   workbenchMentionUtils.tsx, WorkbenchPromptPreview.tsx, WorkbenchSelectChips.tsx
// and is now imported at the top of this file.

const MODE_ICONS: Record<WorkbenchMode, ReactNode> = {
  chat: <MessageOutlined />,
  image: <PictureOutlined />,
  video: <VideoCameraOutlined />,
};

function formatCreditValue(value: number): string {
  if (!Number.isFinite(value)) return "-";
  if (value >= 100) return Math.round(value).toLocaleString("zh-CN");
  return Number(value.toFixed(2)).toString();
}

function WorkbenchPage({
  isAuthenticated,
  session,
  onboarding,
  onEndOnboarding,
  onRequireLogin,
  onOpenResultInCanvas,
  onRefreshUsage,
  resetToken,
}: WorkbenchPageProps) {
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const referenceInputRef = useRef<HTMLInputElement | null>(null);
  const referenceRefsRef = useRef<HTMLDivElement | null>(null);
  const toolbarRef = useRef<HTMLDivElement | null>(null);
  const messagesEndRef = useRef<HTMLDivElement | null>(null);
  const messagesSurfaceRef = useRef<HTMLDivElement | null>(null);
  const referenceItemsRef = useRef<ReferenceItem[]>([]);
  const referenceTokenSequenceRef = useRef<Record<ReferenceKind, number>>({
    image: 0,
    video: 0,
    audio: 0,
    file: 0,
  });
  const generationAbortRef = useRef<AbortController | null>(null);
  const activeConversationIdRef = useRef<number | null>(null);
  const messagesRef = useRef<ChatMessage[]>([]);
  const conversationMessagesCacheRef = useRef<Map<number, ChatMessage[]>>(new Map());
  const skipConversationAutoSelectRef = useRef(Boolean(resetToken));
  const keepaliveTasksRef = useRef<Record<string, WorkbenchKeepaliveTask>>(readStoredKeepaliveTasks());
  const taskAbortControllersRef = useRef<Map<string, AbortController>>(new Map());
  const lastScrollTopRef = useRef(0);
  const scrollActionHintTimerRef = useRef<number | null>(null);
  const shouldFollowNewMessagesRef = useRef(true);
  const pendingScrollToLatestRef = useRef(true);
  const genTracker = useGenerationTasks({ sourceView: "workbench" });
  const renderedMessageIdsRef = useRef<string[]>([]);
  const hasHandledInitialMessagesRef = useRef(false);

  // Onboarding signal — init from prop or localStorage
  const [effectiveOnboarding, setEffectiveOnboarding] = useState(
    () => onboarding || (() => { try { return window.localStorage.getItem("omniai:onboarding") === "1"; } catch { return false; } })(),
  );

  // Track whether onboarding prop was ever true, to avoid overwriting localStorage-initiated true
  const obWasActiveRef = useRef(onboarding);
  useEffect(() => {
    if (onboarding) {
      obWasActiveRef.current = true;
      setEffectiveOnboarding(true);
    } else if (obWasActiveRef.current) {
      // Only deactivate when prop transitions true→false (user dismissed)
      setEffectiveOnboarding(false);
      obWasActiveRef.current = false;
    }
    // If prop was never true, don't touch effectiveOnboarding (preserves localStorage init)
  }, [onboarding]);

  // Poll localStorage as a fallback (handles cases where prop isn't propagated)
  useEffect(() => {
    if (effectiveOnboarding) return;
    const check = () => {
      try {
        if (window.localStorage.getItem("omniai:onboarding") === "1") {
          setEffectiveOnboarding(true);
        }
      } catch {}
    };
    check();
    const interval = setInterval(check, 200);
    return () => clearInterval(interval);
  }, [effectiveOnboarding]);

  const [activeMode, setActiveMode] = useState<WorkbenchMode>(() => effectiveOnboarding ? "chat" : "video");
  const [inputValue, setInputValue] = useState("");
  const [messages, setMessages] = useState<ChatMessage[]>(() => (resetToken ? [] : readStoredMessages()));
  const [promptHistory, setPromptHistory] = useState<string[]>(() => readStoredPromptHistory());
  const [toolbarMenuId, setToolbarMenuId] = useState<ToolbarMenuId>(null);
  const [referenceItems, setReferenceItems] = useState<ReferenceItem[]>([]);
  const [referencePreviewOpen, setReferencePreviewOpen] = useState(false);
  const [isComposerDragging, setIsComposerDragging] = useState(false);
  const composerDragCounterRef = useRef(0);
  const [messagePreviewAttachment, setMessagePreviewAttachment] = useState<ChatAttachment | null>(null);
  const [selectedPromptCase, setSelectedPromptCase] = useState<PromptCaseViewModel | null>(null);
  const [serverPromptCases, setServerPromptCases] = useState<PromptCaseViewModel[]>([]);
  const [promptCaseStatus, setPromptCaseStatus] = useState<"loading" | "ready" | "error">("loading");
  const [promptCaseMeasuredRatios, setPromptCaseMeasuredRatios] = useState<Record<string, number>>({});
  const [mentionPanelPlacement, setMentionPanelPlacement] = useState<"above" | "below">("above");
  const [isGenerating, setIsGenerating] = useState(false);
  const [generationStatus, setGenerationStatus] = useState("准备就绪");
  const [showRechargeModal, setShowRechargeModal] = useState(false);
  const [RechargeModal, setRechargeModal] = useState<RechargeModalComponent | null>(null);
  const [savedAssetMentionItems, setSavedAssetMentionItems] = useState<
    Pick<ReferenceItem, "id" | "kind" | "name" | "previewUrl" | "remoteUrl" | "token">[]
  >([]);

  const [projectError, setProjectError] = useState<string | null>(null);
  const [conversations, setConversations] = useState<ConversationSummary[]>([]);
  const [activeConversationId, setActiveConversationId] = useState<number | null>(() =>
    resetToken ? null : readStoredActiveConversationId(readStoredMessages()),
  );
  const [sidebarCollapsed, setSidebarCollapsed] = useState(true);
  const [deleteDialog, setDeleteDialog] = useState<DeleteDialogState | null>(null);
  const [deleteSubmitting, setDeleteSubmitting] = useState(false);
  // ── Onboarding tour state ──────────────────────────────
  const [tourPhase, setTourPhase] = useState<TourPhaseId>("chat");
  const [tourStep, setTourStep] = useState(0);

  // Sync activeMode with tour phase and keep home view during onboarding
  useEffect(() => {
    if (!effectiveOnboarding) return;
    // Reset tour state for repeat runs
    setTourPhase("chat");
    setTourStep(0);
    // Force "今天想生成什么？" home view — prevent conversation auto-select
    skipConversationAutoSelectRef.current = true;
    setWorkspaceStarted(false);
    setActiveConversationId(null);
    activeConversationIdRef.current = null;
    persistActiveConversationId(null);
    messagesRef.current = [];
    setMessages([]);
  }, [effectiveOnboarding]);

  useEffect(() => {
    if (effectiveOnboarding) {
      if (tourPhase === "chat") setActiveMode("chat");
      else if (tourPhase === "image") setActiveMode("image");
      else if (tourPhase === "video") setActiveMode("video");
    }
  }, [effectiveOnboarding, tourPhase]);
  // ───────────────────────────────────────────────────────
  const [, setGenerationProgress] = useState(0);
  const [cursorIndex, setCursorIndex] = useState(0);
  const [promptSelectionRange, setPromptSelectionRange] = useState({ start: 0, end: 0 });
  const [mentionActiveIndex, setMentionActiveIndex] = useState(0);
  const [composerHidden, setComposerHidden] = useState(false);
  const [scrollActionHint, setScrollActionHint] = useState<"top" | "bottom" | null>(null);
  const [workspaceStarted, setWorkspaceStarted] = useState(false);
  const lastResetTokenRef = useRef(resetToken);

  useEffect(() => {
    activeConversationIdRef.current = activeConversationId;
  }, []);

  useEffect(() => {
    if (!showRechargeModal || RechargeModal) return;

    let cancelled = false;
    void loadRechargeModal().then((component) => {
      if (!cancelled) {
        setRechargeModal(() => component);
      }
    });

    return () => {
      cancelled = true;
    };
  }, [RechargeModal, showRechargeModal]);

  useEffect(() => {
    if (!isAuthenticated) return;
    let cancelled = false;
    assetClient
      .list()
      .then((assets) => {
        if (cancelled) return;
        const items = assets
          .filter((a) => a.url && (a.type === "scene" || a.type === "video" || a.type === "image" || a.type === "character" || a.type === "prop"))
          .slice(0, 20)
          .map((a, i) => {
            const kind: ReferenceKind = a.type === "video" ? "video" : "image";
            return {
              id: `asset-${a.id}`,
              kind,
              name: a.name || `素材 ${i + 1}`,
              previewUrl: a.url || undefined,
              remoteUrl: a.url || undefined,
              token: `@素材${i + 1}`,
            };
          });
        setSavedAssetMentionItems(items);
      })
      .catch(() => {
        // Silently ignore — saved assets are optional enhancement
      });
    return () => {
      cancelled = true;
    };
  }, []);

  const fallbackImageModelOptions = useMemo(
    () => filterImageModelOptionsForSession(IMAGE_MODEL_OPTIONS, session),
    [session],
  );
  const fallbackImageModelValue = fallbackImageModelOptions[0]?.value || IMAGE_MODEL_OPTIONS[0].value;

  const [imageModelOptions, setImageModelOptions] = useState<WorkbenchOption[]>(() => fallbackImageModelOptions);
  const [videoModelOptions, setVideoModelOptions] = useState<WorkbenchOption[]>(VIDEO_MODEL_OPTIONS);
  const [imageModel, setImageModel] = useState(fallbackImageModelValue);
  const [imageRatio, setImageRatio] = useState("16:9");
  const [imageQuality, setImageQuality] = useState(() => getDefaultImageQuality(fallbackImageModelValue));
  const [imageGridMode, setImageGridMode] = useState("single");

  const [videoModel, setVideoModel] = useState(VIDEO_MODEL_OPTIONS[0].value);
  const [videoFrameMode, setVideoFrameMode] = useState("omni");
  const [videoRatio, setVideoRatio] = useState("16:9");
  const [videoDuration, setVideoDuration] = useState("5");
  const [videoQuality, setVideoQuality] = useState(() => getDefaultVideoQuality(VIDEO_MODEL_OPTIONS[0].value));

  const [chatModel, setChatModel] = useState(CHAT_MODEL_OPTIONS[0].value);
  const [modelPrices, setModelPrices] = useState<PublicModelPrice[]>([]);
  const [enterpriseVideoPricing, setEnterpriseVideoPricing] = useState<EnterpriseVideoPricingConfig | null>(null);
  const [thinkingSpeed, setThinkingSpeed] = useState(THINKING_SPEED_OPTIONS[0].value);
  const [thinkingDepth, setThinkingDepth] = useState(THINKING_DEPTH_OPTIONS[0].value);

  useEffect(() => {
    let cancelled = false;

    publicPricingClient
      .getPricing()
      .then((pricing) => {
        if (cancelled) return;
        setModelPrices(pricing.modelPrices);
        setEnterpriseVideoPricing(pricing.enterpriseVideoPricing);
      })
      .catch(() => {
        if (cancelled) return;
        setModelPrices([]);
        setEnterpriseVideoPricing(null);
      });

    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    let cancelled = false;

    const applyImageModels = (models: WorkbenchOption[]) => {
      const filteredImageModels = filterImageModelOptionsForSession(
        models.length ? models : IMAGE_MODEL_OPTIONS,
        session,
      );
      const nextImageModels = filteredImageModels.length ? filteredImageModels : fallbackImageModelOptions;
      setImageModelOptions(nextImageModels);
      setImageModel((current) => {
        if (nextImageModels.some((item) => item.value === current)) return current;
        const nextValue = nextImageModels[0]?.value || fallbackImageModelValue;
        setImageQuality(getDefaultImageQuality(nextValue));
        return nextValue;
      });
    };

    if (!isAuthenticated) {
      applyImageModels(IMAGE_MODEL_OPTIONS);
      return () => {
        cancelled = true;
      };
    }

    modelCapabilitiesClient
      .get()
      .then((capabilities) => {
        if (cancelled) return;
        const nextVideoModels = capabilities.videoModels.length ? capabilities.videoModels : VIDEO_MODEL_OPTIONS;

        applyImageModels(capabilities.imageModels);
        setVideoModelOptions(nextVideoModels);
        setVideoModel((current) => {
          const normalizedCurrent = toHappyHorseDisplayModel(current);
          if (nextVideoModels.some((item) => item.value === normalizedCurrent)) return normalizedCurrent;
          const nextValue = nextVideoModels[0]?.value || VIDEO_MODEL_OPTIONS[0].value;
          setVideoQuality(getDefaultVideoQuality(nextValue));
          return nextValue;
        });
      })
      .catch(() => {});

    return () => {
      cancelled = true;
    };
  }, [fallbackImageModelValue, isAuthenticated, session]);

  const toolTheme = MODE_META[activeMode];
  const workbenchAccent = "#00ff88";
  const hasConversationRecords = activeConversationId !== null || messages.length > 0;
  const hasActivatedWorkspace = !effectiveOnboarding && (workspaceStarted || isGenerating || hasConversationRecords);
  const activeVideoModelValue = toHappyHorseDisplayModel(videoModel);
  const activeModelValue =
    activeMode === "image" ? imageModel : activeMode === "video" ? activeVideoModelValue : chatModel;
  const activeModel =
    activeMode === "image"
      ? imageModelOptions.find((item) => item.value === imageModel)?.label || imageModel
      : activeMode === "video"
        ? videoModelOptions.find((item) => item.value === activeVideoModelValue)?.label || activeVideoModelValue
        : CHAT_MODEL_OPTIONS.find((item) => item.value === chatModel)?.label || chatModel;
  const conversationRecords = useMemo<WebProjectSummary[]>(
    () =>
      conversations.map((conversation) => ({
        id: String(conversation.id),
        name: conversation.title,
        description: null,
        thumbnailUrl: null,
        updatedAt: conversation.updatedAt,
        storyboardCount: 0,
        imageCount: 0,
        videoCount: 0,
        source: "server",
        mode: conversation.mode,
      })),
    [conversations],
  );
  const hasSidebarRecords = conversationRecords.length > 0;

  const activeConversationTitle = useMemo(() => {
    if (!activeConversationId) return "";
    return conversations.find((c) => c.id === activeConversationId)?.title || "";
  }, [conversations, activeConversationId]);

  const downloadFilenameBase = useMemo(() => {
    const today = new Date();
    const dateStr = `${today.getFullYear()}-${String(today.getMonth() + 1).padStart(2, "0")}-${String(today.getDate()).padStart(2, "0")}`;
    return activeConversationTitle ? `${activeConversationTitle}_${dateStr}` : dateStr;
  }, [activeConversationTitle]);

  useEffect(() => {
    setSidebarCollapsed(!hasSidebarRecords);
  }, [hasSidebarRecords]);

  const hasImageReferences = activeMode === "image" && referenceItems.some((item) => item.kind === "image");
  const isImageGridMode = activeMode === "image" && imageGridMode !== "single";
  const imageQualityContext = useMemo(
    () => ({
      hasReferenceImages: hasImageReferences,
      isGridMode: isImageGridMode,
    }),
    [hasImageReferences, isImageGridMode],
  );
  const imageQualityOptions = useMemo(
    () => getImageQualityOptionsForContext(imageModel, imageQualityContext),
    [imageModel, imageQualityContext],
  );
  const imageGridModeOptions = useMemo(
    () =>
      String(imageModel || "").toLowerCase().startsWith("wan2.7-")
        ? GRID_MODE_OPTIONS.filter((option) => option.value !== "grid-25")
        : GRID_MODE_OPTIONS,
    [imageModel],
  );
  const videoQualityOptions = getVideoQualityOptions(videoModel);
  const videoQualityLabel = getVideoQualityLabel(videoModel, videoQuality);

  const imageSettingsSummary = `${imageRatio} / ${imageQuality}`;
  const selectedChatTokenRate = useMemo(
    () => resolveTextTokenCreditRate(modelPrices, chatModel) || FALLBACK_TEXT_TOKEN_CREDIT_RATE,
    [chatModel, modelPrices],
  );
  const billingEstimate = useMemo(() => {
    if (activeMode === "image") {
      return {
        label: "预计 20 积分",
        title: `图片生成按任务计费：${activeModel}，${imageSettingsSummary}，预计 20 积分`,
      };
    }
    if (activeMode === "video") {
      try {
        const durationSeconds = Math.max(1, Math.ceil(Number(videoDuration) || 1));
        const credits = calculateEnterpriseVideoCredits({
          model: activeModelValue,
          resolution: videoQuality,
          durationSeconds,
          muted: false,
          hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
        }, enterpriseVideoPricing || undefined);
        return {
          label: `预计 ${formatCreditValue(credits)} 积分`,
          title: `${activeModel}，${videoQualityLabel}，${durationSeconds} 秒，预计 ${formatCreditValue(credits)} 积分`,
        };
      } catch {
        return {
          label: "计费以提交后为准",
          title: "当前模型的预估计费暂不可用，实际扣费以服务端结算为准",
        };
      }
    }
    const textBillingPrefix =
      selectedChatTokenRate.source === "server" ? "文本计费" : "服务端价格暂不可用，按默认预估";
    return {
      label: "按 Token 结算",
      title: `${textBillingPrefix}：${activeModel}，${formatTextTokenCreditRule(selectedChatTokenRate)}`,
    };
  }, [
    activeMode,
    activeModel,
    activeModelValue,
    imageSettingsSummary,
    enterpriseVideoPricing,
    referenceItems,
    selectedChatTokenRate,
    videoDuration,
    videoQuality,
    videoQualityLabel,
  ]);
  const composerPlaceholder =
    referenceItems.length > 0 ? `${toolTheme.placeholder}，可输入 @ 引用参考内容` : toolTheme.placeholder;
  const dropdownDirection = hasActivatedWorkspace ? "up" : "down";
  const accentRgb = hexToRgbTriplet(workbenchAccent);
  const themeVars = {
    "--accent": workbenchAccent,
    "--accent-rgb": accentRgb,
    "--accent-muted": `rgba(${accentRgb}, 0.12)`,
    "--border-accent": `rgba(${accentRgb}, 0.24)`,
    "--accent-glow": `0 0 24px rgba(${accentRgb}, 0.22)`,
  } as CSSProperties;

  const scrollMessagesToLatest = useCallback((behavior: ScrollBehavior = "smooth") => {
    const scroll = () => {
      const surface = messagesSurfaceRef.current;
      if (!surface) {
        messagesEndRef.current?.scrollIntoView({ block: "end", behavior });
        return;
      }

      setComposerHidden(false);
      shouldFollowNewMessagesRef.current = true;
      surface.scrollTo({ top: surface.scrollHeight, behavior });
      lastScrollTopRef.current = surface.scrollTop;
    };

    window.requestAnimationFrame(() => {
      scroll();
      window.setTimeout(scroll, 80);
    });
  }, []);

  const hideScrollActionHint = useCallback(() => {
    if (scrollActionHintTimerRef.current !== null) {
      window.clearTimeout(scrollActionHintTimerRef.current);
      scrollActionHintTimerRef.current = null;
    }
    setScrollActionHint(null);
  }, []);

  const showScrollActionHint = useCallback((direction: "top" | "bottom") => {
    if (scrollActionHintTimerRef.current !== null) {
      window.clearTimeout(scrollActionHintTimerRef.current);
    }
    setScrollActionHint(direction);
    scrollActionHintTimerRef.current = window.setTimeout(() => {
      setScrollActionHint(null);
      scrollActionHintTimerRef.current = null;
    }, 1400);
  }, []);

  useEffect(() => () => {
    if (scrollActionHintTimerRef.current !== null) {
      window.clearTimeout(scrollActionHintTimerRef.current);
    }
  }, []);

  const imageSettingGroups = useMemo<WorkbenchFieldGroup[]>(
    () => [
      {
        label: "比例",
        value: imageRatio,
        options: RATIO_OPTIONS,
        onChange: setImageRatio,
        kind: "ratio",
        columns: 3,
        icon: <AppstoreOutlined />,
      },
      {
        label: "清晰度",
        value: imageQuality,
        options: imageQualityOptions,
        onChange: setImageQuality,
        kind: "pill",
        columns: imageQualityOptions.length >= 3 ? 3 : 2,
        icon: <PictureOutlined />,
      },
    ],
    [imageQuality, imageQualityOptions, imageRatio],
  );

  const videoRatioGroups = useMemo<WorkbenchFieldGroup[]>(
    () => [
      {
        label: "比例",
        value: videoRatio,
        options: RATIO_OPTIONS.filter((item) => item.value !== "3:4"),
        onChange: setVideoRatio,
        kind: "ratio",
        columns: 3,
        icon: <AppstoreOutlined />,
      },
    ],
    [videoRatio],
  );

  const promptMentionOptions = useMemo(() => {
    const refOptions = referenceItems.map((item) => ({
      token: item.token,
      id: item.id,
      name: item.name,
      kind: item.kind,
      previewUrl: item.previewUrl,
      remoteUrl: item.remoteUrl,
    }));
    const existingTokens = new Set(refOptions.map((o) => o.token));
    const assetOptions = savedAssetMentionItems.filter((item) => !existingTokens.has(item.token));
    return [...refOptions, ...assetOptions];
  }, [referenceItems, savedAssetMentionItems]);
  const promptCaseDisplayItems = useMemo(() => {
    return serverPromptCases;
  }, [serverPromptCases]);

  const handlePromptCaseImageLoad = useCallback((itemId: string, event: SyntheticEvent<HTMLImageElement>) => {
    const { naturalWidth, naturalHeight } = event.currentTarget;
    if (!naturalWidth || !naturalHeight) return;
    const measuredRatio = naturalWidth / naturalHeight;
    setPromptCaseMeasuredRatios((current) => {
      const previousRatio = current[itemId];
      if (previousRatio && Math.abs(previousRatio - measuredRatio) < 0.01) return current;
      return {
        ...current,
        [itemId]: measuredRatio,
      };
    });
  }, []);

  const referenceUploadLabel = getReferenceUploadLabel(activeMode);
  const referenceButtonLabel =
    referenceItems.length > 0 ? (referencePreviewOpen ? "点击折叠" : "点击显示") : referenceUploadLabel;
  const referenceLimit = getReferenceLimit(activeMode, videoFrameMode);

  const promptBeforeCursor = inputValue.slice(0, cursorIndex);
  const promptMentionMatch = detectMentionTrigger(promptBeforeCursor);
  const promptMentionOpen = promptMentionMatch !== null;
  const promptPreviewNodes = useMemo(
    () => renderPromptPreviewNodes(inputValue, promptMentionOptions),
    [inputValue, promptMentionOptions],
  );
  const promptMentionTokenRanges = useMemo(
    () => getPromptMentionTokenRanges(inputValue, promptMentionOptions),
    [inputValue, promptMentionOptions],
  );
  const hasPromptSelection = promptSelectionRange.start !== promptSelectionRange.end;
  const showPromptPreview =
    !hasPromptSelection &&
    inputValue.length > 0 &&
    promptPreviewNodes.length > 0 &&
    /@(图片|视频|音频|附件|素材)\d+/.test(inputValue);

  useEffect(() => {
    let cancelled = false;
    setPromptCaseStatus("loading");
    communityClient
      .listApprovedCases({ limit: 100, tag: "生成页面社区", sort: "latest" })
      .then((items) => {
        if (cancelled) return;
        setServerPromptCases(
          items
            .map(communityCaseToPromptCase)
            .filter((item): item is PromptCaseViewModel => Boolean(item)),
        );
        setPromptCaseStatus("ready");
      })
      .catch(() => {
        if (!cancelled) {
          setServerPromptCases([]);
          setPromptCaseStatus("error");
        }
      });

    return () => {
      cancelled = true;
    };
  }, []);

  useEffect(() => {
    if (!videoQualityOptions.some((item) => item.value === videoQuality)) {
      setVideoQuality(getDefaultVideoQuality(videoModel));
    }
  }, [videoModel, videoQuality, videoQualityOptions]);

  useEffect(() => {
    if (!messagePreviewAttachment) return undefined;

    const handleKeyDown = (event: globalThis.KeyboardEvent) => {
      if (event.key === "Escape") {
        setMessagePreviewAttachment(null);
      }
    };

    window.addEventListener("keydown", handleKeyDown);
    return () => window.removeEventListener("keydown", handleKeyDown);
  }, [messagePreviewAttachment]);

  useEffect(() => {
    if (!promptMentionOpen) return undefined;

    const updatePlacement = () => {
      const anchor = textareaRef.current;
      const rect = anchor?.getBoundingClientRect();
      if (!rect) return;
      const estimatedHeight = promptMentionOptions.length > 0 ? Math.min(240, 116 + promptMentionOptions.length * 44) : 152;
      const topSpace = rect.top;
      const bottomSpace = window.innerHeight - rect.bottom;
      setMentionPanelPlacement(topSpace < estimatedHeight + 24 || bottomSpace > topSpace ? "below" : "above");
    };

    updatePlacement();
    window.addEventListener("resize", updatePlacement);
    window.addEventListener("scroll", updatePlacement, true);
    return () => {
      window.removeEventListener("resize", updatePlacement);
      window.removeEventListener("scroll", updatePlacement, true);
    };
  }, [promptMentionOpen, promptMentionOptions.length]);

  useEffect(() => {
    if (!isAuthenticated) {
      Object.values(keepaliveTasksRef.current).forEach((task) => {
        taskAbortControllersRef.current.get(task.taskId)?.abort();
        taskAbortControllersRef.current.delete(task.taskId);
      });
      generationAbortRef.current?.abort();
      generationAbortRef.current = null;
      keepaliveTasksRef.current = {};
      skipConversationAutoSelectRef.current = false;
      clearWorkbenchLocalState();
      referenceItemsRef.current.forEach((item) => disposeReferencePreview(item));
      referenceItemsRef.current = [];
      setConversations([]);
      setActiveConversationId(null);
      activeConversationIdRef.current = null;
      conversationMessagesCacheRef.current.clear();
      messagesRef.current = [];
      setMessages([]);
      setInputValue("");
      setReferenceItems([]);
      setReferencePreviewOpen(false);
      setMessagePreviewAttachment(null);
      setWorkspaceStarted(false);
      setComposerHidden(false);
      setIsGenerating(false);
      setGenerationStatus("准备就绪");
      setGenerationProgress(0);
      setProjectError(null);
      return;
    }
  }, [isAuthenticated]);

  const loadConversations = useCallback(async () => {
    if (!isAuthenticated) {
      setConversations([]);
      return;
    }
    try {
      const list = await conversationClient.list();
      setConversations(list);
      const restoredId = activeConversationIdRef.current || readStoredActiveConversationId(messagesRef.current);
      if (restoredId && list.some((conversation) => conversation.id === restoredId)) {
        setActiveConversationId(restoredId);
        activeConversationIdRef.current = restoredId;
        persistActiveConversationId(restoredId);
        setWorkspaceStarted(true);
      } else if (restoredId && !list.some((conversation) => conversation.id === restoredId)) {
        setActiveConversationId(null);
        activeConversationIdRef.current = null;
        persistActiveConversationId(null);
      } else if (
        list.length > 0 &&
        !activeConversationIdRef.current &&
        messagesRef.current.length === 0 &&
        !skipConversationAutoSelectRef.current
      ) {
        const nextConversationId = list[0].id;
        setActiveConversationId(nextConversationId);
        activeConversationIdRef.current = nextConversationId;
        persistActiveConversationId(nextConversationId);
        setWorkspaceStarted(true);
      }
      setProjectError(null);
    } catch (error) {
      if (isAuthFailure(error)) {
        setProjectError(null);
        return;
      }
      setProjectError(error instanceof Error ? error.message : String(error));
    }
  }, [isAuthenticated]);

  const loadConversation = useCallback(async (conversationId: number) => {
    try {
      skipConversationAutoSelectRef.current = false;
      const conversation = await conversationClient.get(conversationId);
      const nextMessages = conversation.messages.filter((item): item is ChatMessage => {
        return (
          item &&
          typeof item.id === "string" &&
          (item.role === "user" || item.role === "assistant") &&
          typeof item.body === "string"
        );
      }) as ChatMessage[];
      setActiveConversationId(conversation.id);
      activeConversationIdRef.current = conversation.id;
      pendingScrollToLatestRef.current = true;
      setMessages(nextMessages);
      rememberConversationMessages(conversation.id, nextMessages);
      setWorkspaceStarted(nextMessages.length > 0);
      setComposerHidden(false);
      setProjectError(null);
    } catch (error) {
      if (isAuthFailure(error)) {
        setProjectError(null);
        return;
      }
      setProjectError(error instanceof Error ? error.message : String(error));
    }
  }, []);

  // Migrate orphaned localStorage messages (no conversationId) to server
  const migrateOrphanedMessages = useCallback(async () => {
    const stored = readStoredMessages();
    if (stored.length === 0) return;
    const hasConversationId = stored.some((m) => m.conversationId);
    if (hasConversationId) return; // messages already linked to a server conversation

    try {
      const conv = await conversationClient.create(
        stored[0]?.body?.slice(0, 20) || "历史对话",
        stored[0]?.mode || "chat",
        stored.map((m) => ({ ...m, conversationId: undefined })),
      );
      const migrated = stored.map((m) => ({ ...m, conversationId: conv.id }));
      setMessages(migrated);
      messagesRef.current = migrated;
      rememberConversationMessages(conv.id, migrated);
      setActiveConversationId(conv.id);
      activeConversationIdRef.current = conv.id;
      persistActiveConversationId(conv.id);
      persistMessages(migrated);
      void loadConversations();
    } catch {
      // Migration is best-effort; if it fails, keep using localStorage
    }
  }, [loadConversations]);

  useEffect(() => {
    if (!isAuthenticated) return;
    void migrateOrphanedMessages();
  }, [isAuthenticated, migrateOrphanedMessages]);

  useEffect(() => {
    void loadConversations();
  }, [loadConversations]);

  useEffect(() => {
    if (!isAuthenticated || !activeConversationId || messages.length > 0) return;
    void loadConversation(activeConversationId);
  }, [activeConversationId, isAuthenticated, loadConversation, messages.length]);

  const saveProjectMessages = useCallback(
    async (nextMessages: ChatMessage[], saveReason = "web-chat-autosave") => {
      const conversationId = activeConversationIdRef.current;
      if (!conversationId) return;
      const persistableMessages = getPersistableMessages(nextMessages);
      if (persistableMessages.length === 0) {
        return;
      }
      await conversationClient.update(conversationId, { messages: persistableMessages as never[] });
      setProjectError(null);
      void loadConversations();
      return { saveReason };
    },
    [loadConversations],
  );

  const handleProjectSaveError = useCallback((error: unknown) => {
    if (isAuthFailure(error)) {
      setProjectError(null);
      return;
    }
    setProjectError(getErrorText(error));
  }, []);

  const rememberConversationMessages = (conversationId: number, nextMessages: ChatMessage[]) => {
    conversationMessagesCacheRef.current.set(conversationId, nextMessages);
  };

  const getActiveKeepaliveTask = (conversationId: number | null) => {
    if (!conversationId) return undefined;
    return Object.values(keepaliveTasksRef.current).find((task) => task.conversationId === conversationId);
  };

  const syncActiveGenerationUi = useCallback((conversationId: number | null = activeConversationIdRef.current) => {
    const activeTask = getActiveKeepaliveTask(conversationId);
    setIsGenerating(Boolean(activeTask));
    if (activeTask) {
      setGenerationStatus(activeTask.statusLabel);
      setGenerationProgress(activeTask.progress);
    } else {
      setGenerationProgress(0);
    }
  }, []);

  const patchConversationMessage = useCallback(
    async (conversationId: number, messageId: string, patch: Partial<ChatMessage>) => {
      const sourceMessages =
        activeConversationIdRef.current === conversationId
          ? messagesRef.current
          : conversationMessagesCacheRef.current.get(conversationId);

      if (!sourceMessages) return;

      let changed = false;
      const nextMessages = sourceMessages.map((message) => {
        if (message.id !== messageId) return message;
        changed = true;
        const nextPatch = { ...patch };
        if (
          typeof nextPatch.taskProgress === "number" &&
          typeof message.taskProgress === "number" &&
          nextPatch.taskProgress < message.taskProgress &&
          nextPatch.status !== "failed"
        ) {
          nextPatch.taskProgress = message.taskProgress;
        }
        return { ...message, ...nextPatch };
      });
      if (!changed) return;

      conversationMessagesCacheRef.current.set(conversationId, nextMessages);
      if (activeConversationIdRef.current === conversationId) {
        setMessages(nextMessages);
      }
      if (shouldPersistPatch(patch)) {
        await saveProjectMessages(nextMessages).catch(handleProjectSaveError);
      }
    },
    [handleProjectSaveError, saveProjectMessages],
  );

  const upsertKeepaliveTask = (task: WorkbenchKeepaliveTask) => {
    keepaliveTasksRef.current = { ...keepaliveTasksRef.current, [task.taskId]: task };
    persistKeepaliveTasks(keepaliveTasksRef.current);
  };

  const removeKeepaliveTask = (taskId: string) => {
    const removed = keepaliveTasksRef.current[taskId];
    releaseGenerationSlot(removed?.concurrencySlotId);
    const { [taskId]: _removed, ...rest } = keepaliveTasksRef.current;
    keepaliveTasksRef.current = rest;
    persistKeepaliveTasks(rest);
  };

  const releaseKeepaliveTaskLocally = useCallback((taskId: string, options?: { cancelServer?: boolean }) => {
    const task = keepaliveTasksRef.current[taskId];
    taskAbortControllersRef.current.get(taskId)?.abort();
    taskAbortControllersRef.current.delete(taskId);
    removeKeepaliveTask(taskId);
    if (task && options?.cancelServer) {
      aiGenerationClient.cancelTask(task.taskId).catch(() => {});
    }
    syncActiveGenerationUi();
  }, [syncActiveGenerationUi]);

  const releaseKeepaliveTaskAfterNetworkLoss = useCallback((task: WorkbenchKeepaliveTask, progress: number) => {
    const latestTask = {
      ...task,
      progress,
      statusLabel: "网络中断，已释放提交按钮",
    };
    void patchConversationMessage(task.conversationId, task.assistantMessageId, {
      status: "failed",
      taskProgress: Math.max(progress, 100),
      taskStatusLabel: "网络中断",
      body: "网络中断，当前任务已停止等待并释放提交按钮。请确认网络恢复后重新提交任务。",
    });
    upsertKeepaliveTask(latestTask);
    releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
    if (activeConversationIdRef.current === task.conversationId) {
      setIsGenerating(false);
      setGenerationStatus("网络中断，已释放提交按钮");
      setGenerationProgress(0);
    }
  }, [patchConversationMessage, releaseKeepaliveTaskLocally]);

  const cancelActiveKeepaliveTasksOnPageExit = useCallback(() => {
    const tasks = Object.values(keepaliveTasksRef.current);
    if (!tasks.length) return;
    tasks.forEach((task) => {
      taskAbortControllersRef.current.get(task.taskId)?.abort();
      taskAbortControllersRef.current.delete(task.taskId);
      releaseGenerationSlot(task.concurrencySlotId);
      aiGenerationClient.cancelTaskOnUnload(task.taskId);
    });
    keepaliveTasksRef.current = {};
    persistKeepaliveTasks({});
    setIsGenerating(false);
    setGenerationStatus("已释放未完成任务");
    setGenerationProgress(0);
  }, []);

  const runKeepalivePoll = useCallback(
    (task: WorkbenchKeepaliveTask) => {
      if (taskAbortControllersRef.current.has(task.taskId)) return;

      let lastKnownProgress = Math.max(0, Number(task.progress || 0));
      let taskPollFailures = 0;
      let lastProgressAt = task.startedAt || Date.now();
      const taskKind = task.mode === "image" ? "image" : "video";
      const timeoutPolicy = getTaskTimeoutPolicy({ kind: taskKind, model: task.modelLabel, operation: task.operation });
      const abortController = new AbortController();
      taskAbortControllersRef.current.set(task.taskId, abortController);
      if (activeConversationIdRef.current === task.conversationId) {
        generationAbortRef.current = abortController;
        setIsGenerating(true);
        setGenerationStatus(task.statusLabel);
        setGenerationProgress(lastKnownProgress);
      }

      const sleep = (ms: number) => new Promise((resolve) => window.setTimeout(resolve, ms));

      void (async () => {
        try {
          for (let attempt = 0; attempt < 220; attempt += 1) {
            if (abortController.signal.aborted) return;
            if (attempt > 0) await sleep(3000);
            if (abortController.signal.aborted) return;
            if (typeof navigator !== "undefined" && navigator.onLine === false) {
              releaseKeepaliveTaskAfterNetworkLoss(task, lastKnownProgress);
              return;
            }

            let status;
            try {
              status = await aiGenerationClient.getTaskStatus(task.taskId);
            } catch {
              taskPollFailures += 1;
              if (taskPollFailures >= WORKBENCH_TASK_MAX_POLL_FAILURES) {
                await patchConversationMessage(task.conversationId, task.assistantMessageId, {
                  body: "任务状态连续查询失败，已停止等待。请重新生成或稍后在资产页查看。",
                  status: "failed",
                  taskProgress: 100,
                  taskStatusLabel: "任务异常",
                });
                releaseKeepaliveTaskLocally(task.taskId, { cancelServer: true });
                onRefreshUsage?.();
                return;
              }
              continue;
            }
            taskPollFailures = 0;

            const currentMessageProgress =
              activeConversationIdRef.current === task.conversationId
                ? messagesRef.current.find((message) => message.id === task.assistantMessageId)?.taskProgress || 0
                : 0;
            const serverProgress = Number(status.progress || 0);
            const baseProgress = Number.isFinite(serverProgress) ? serverProgress : 0;
            const progress = status.status === "completed"
              ? 100
              : Math.min(99, Math.max(10, lastKnownProgress, currentMessageProgress, Math.round(baseProgress)));
            if (progress > lastKnownProgress || status.status === "completed") {
              lastProgressAt = Date.now();
            }
            lastKnownProgress = Math.max(lastKnownProgress, progress);
            const isSuperResolveTask = task.operation === "video-super-resolution";
            const statusLabel =
              status.status === "pending"
                ? (isSuperResolveTask ? "超分排队中" : "任务排队中")
                : status.status === "running"
                  ? (isSuperResolveTask ? "超分处理中..." : "正在生成...")
                  : status.status === "completed"
                    ? (isSuperResolveTask ? "超分完成" : "生成完成")
                    : (isSuperResolveTask ? "超分失败" : "生成失败");

            const latestTask = {
              ...task,
              progress,
              statusLabel,
            };
            upsertKeepaliveTask(latestTask);

            if (activeConversationIdRef.current === task.conversationId) {
              setIsGenerating(status.status !== "completed" && status.status !== "failed");
              setGenerationStatus(statusLabel);
              setGenerationProgress(progress);
            }

            const localTimeoutReason = status.status !== "completed" && status.status !== "failed" && status.status !== "cancelled"
              ? isTaskLocallyTimedOut({
                  startedAt: task.startedAt || Date.now(),
                  lastProgressAt,
                  progress,
                  policy: timeoutPolicy,
                })
              : null;
            if (localTimeoutReason) {
              await patchConversationMessage(task.conversationId, task.assistantMessageId, {
                body: buildLocalTimeoutMessage(taskKind),
                status: "local_timeout",
                taskLifecycleStatus: "local_timeout",
                taskRefundStatus: "unknown",
                taskProgress: progress,
                taskStatusLabel: "本地等待超时",
              });
              removeKeepaliveTask(task.taskId);
              onRefreshUsage?.();
              return;
            }

            if (status.status === "completed" && status.resultUrl) {
              const completedPatch: Partial<ChatMessage> = {
                body: isSuperResolveTask
                  ? "视频已完成超分，并已替换为高清版本"
                  : task.mode === "image" ? "图像已经生成完成。" : "视频已经生成完成。",
                status: "completed",
                taskProgress: 100,
                taskStatusLabel: statusLabel,
                resultUrl: status.resultUrl,
                resultType: task.mode,
                resultOriginalUrl: status.resultUrl,
              };
              if (!isSuperResolveTask) {
                completedPatch.result = buildAssistantResult(task.mode, task.modelLabel, task.specs, task.referenceCount);
              }
              await patchConversationMessage(task.conversationId, task.assistantMessageId, completedPatch);
              removeKeepaliveTask(task.taskId);
              onRefreshUsage?.();
              if (status.status === "completed") {
                import("../../utils/generationNotifier").then((m) =>
                  m.notifyTaskCompleted(task.mode === "video" ? "视频" : "图片", task.mode as "image" | "video"),
                );
              }
              try {
                if (status.resultUrl) {
                  const persistedResult = await persistWorkbenchResultAsset({
                    title: task.mode === "image" ? "生成图片" : isSuperResolveTask ? "超分视频" : "生成视频",
                    sourceUrl: status.resultUrl,
                    resultType: task.mode,
                    taskId: task.taskId,
                  });
                  await patchConversationMessage(task.conversationId, task.assistantMessageId, {
                    resultUrl: persistedResult.url,
                    resultOriginalUrl: persistedResult.originalUrl,
                    resultOssKey: persistedResult.ossKey,
                    resultMimeType: persistedResult.mimeType,
                  });
                }
              } catch (error) {
                console.warn("[workbench] result persistence skipped after visible completion:", error);
              }
              return;
            }

            if (status.status === "completed" && !status.resultUrl) {
              await patchConversationMessage(task.conversationId, task.assistantMessageId, {
                body: "任务已结束，但没有返回可用资源。请重新生成。",
                status: "failed",
                taskProgress: 100,
                taskStatusLabel: "结果缺失",
              });
              removeKeepaliveTask(task.taskId);
              return;
            }

            if (status.status === "failed" || status.status === "cancelled") {
              if (getCachedRole() === "admin") console.error("[轮询] 任务失败", { taskId: task.taskId, status, error: status.error });
              if (isInsufficientBalanceMessage(status.error)) setShowRechargeModal(true);
              await patchConversationMessage(task.conversationId, task.assistantMessageId, {
                body: translateTaskError(status.error),
                status: "failed",
                taskProgress: progress,
                taskStatusLabel: statusLabel,
              });
              removeKeepaliveTask(task.taskId);
              onRefreshUsage?.();
              return;
            }

            await patchConversationMessage(task.conversationId, task.assistantMessageId, {
              body: isSuperResolveTask ? "视频超分任务已提交，正在增强画质..." : "任务已提交，正在生成中...",
              status: "thinking",
              taskProgress: progress,
              taskStatusLabel: statusLabel,
            });
          }

          await patchConversationMessage(task.conversationId, task.assistantMessageId, {
            body: task.operation === "video-super-resolution"
              ? "视频超分超时，请稍后重试"
              : "生成超时，请稍后在资产页查看结果。",
            status: "failed",
            taskStatusLabel: task.operation === "video-super-resolution" ? "超分超时" : "生成超时",
          });
          removeKeepaliveTask(task.taskId);
        } finally {
          taskAbortControllersRef.current.delete(task.taskId);
          if (generationAbortRef.current === abortController) {
            generationAbortRef.current = null;
          }
          syncActiveGenerationUi();
        }
      })();
    },
    [patchConversationMessage, syncActiveGenerationUi],
  );

  useEffect(() => {
    persistMessages(messages);
    if (activeConversationIdRef.current) {
      rememberConversationMessages(activeConversationIdRef.current, messages);
    }
    const previousMessageIds = renderedMessageIdsRef.current;
    const currentMessageIds = messages.map((message) => message.id);
    const messageListChanged =
      currentMessageIds.length !== previousMessageIds.length ||
      currentMessageIds.some((messageId, index) => messageId !== previousMessageIds[index]);

    renderedMessageIdsRef.current = currentMessageIds;
    messagesRef.current = messages;

    const shouldForceScrollToLatest = pendingScrollToLatestRef.current && messages.length > 0;
    if (shouldForceScrollToLatest) {
      pendingScrollToLatestRef.current = false;
    }

    if (
      shouldForceScrollToLatest ||
      (hasHandledInitialMessagesRef.current && messageListChanged && shouldFollowNewMessagesRef.current)
    ) {
      scrollMessagesToLatest(shouldForceScrollToLatest ? "auto" : "smooth");
    }

    hasHandledInitialMessagesRef.current = true;
  }, [messages, scrollMessagesToLatest]);

  useEffect(() => {
    activeConversationIdRef.current = activeConversationId;
    persistActiveConversationId(activeConversationId);
    syncActiveGenerationUi(activeConversationId);
  }, [activeConversationId, syncActiveGenerationUi]);

  useEffect(() => {
    if (!imageQualityOptions.some((option) => option.value === imageQuality)) {
      setImageQuality(getDefaultImageQualityForContext(imageModel, imageQualityContext));
    }
  }, [imageModel, imageQuality, imageQualityContext, imageQualityOptions]);

  useEffect(() => {
    if (!imageGridModeOptions.some((option) => option.value === imageGridMode)) {
      setImageGridMode("single");
    }
  }, [imageGridMode, imageGridModeOptions]);

  useEffect(() => {
    if (activeMode !== "video" || videoFrameMode !== "start-end" || referenceItems.length <= 2) return;

    setReferenceItems((current) => {
      if (current.length <= 2) return current;
      const removed = current.slice(2);
      removed.forEach((item) => disposeReferencePreview(item));
      return current.slice(0, 2);
    });
  }, [activeMode, referenceItems.length, videoFrameMode]);

  useEffect(() => {
    Object.values(keepaliveTasksRef.current).forEach((task) => runKeepalivePoll(task));
    return () => {
      taskAbortControllersRef.current.forEach((controller) => controller.abort());
      taskAbortControllersRef.current.clear();
    };
  }, [runKeepalivePoll]);

  useEffect(() => {
    const handlePageHide = () => {
      cancelActiveKeepaliveTasksOnPageExit();
    };
    const handleOnline = () => {
      Object.values(keepaliveTasksRef.current).forEach((task) => runKeepalivePoll(task));
      syncActiveGenerationUi();
    };

    window.addEventListener("pagehide", handlePageHide);
    window.addEventListener("online", handleOnline);
    aiGenerationClient.flushPendingTaskCancellations();
    return () => {
      window.removeEventListener("pagehide", handlePageHide);
      window.removeEventListener("online", handleOnline);
    };
  }, [cancelActiveKeepaliveTasksOnPageExit, runKeepalivePoll, syncActiveGenerationUi]);

  useEffect(() => {
    persistPromptHistory(promptHistory);
  }, [promptHistory]);

  const handleRefreshProject = useCallback(() => {
    void loadConversations();
  }, [loadConversations]);

  const handleRenameProject = useCallback(
    async (projectId: string, title: string) => {
      const conversationId = Number(projectId);
      if (!Number.isFinite(conversationId)) return;
      try {
        await conversationClient.update(conversationId, { title });
        setConversations((prev) => prev.map((c) => c.id === conversationId ? { ...c, title } : c));
        setProjectError(null);
      } catch (error) {
        if (isAuthFailure(error)) {
          setProjectError(null);
          return;
        }
        setProjectError(error instanceof Error ? error.message : String(error));
      }
    },
    [],
  );

  const requestDeleteProject = useCallback(
    (projectId: string) => {
      const record = conversationRecords.find((item) => item.id === projectId);
      setDeleteDialog({ projectId, title: record?.name || projectId });
    },
    [conversationRecords],
  );

  const confirmDeleteProject = useCallback(
    async () => {
      if (!deleteDialog || deleteSubmitting) return;
      const conversationId = Number(deleteDialog.projectId);
      if (!Number.isFinite(conversationId)) {
        setDeleteDialog(null);
        return;
      }
      setDeleteSubmitting(true);
      try {
        await conversationClient.delete(conversationId, { cleanupUserData: true });
        const remainingConversations = conversations.filter((conversation) => conversation.id !== conversationId);
        setConversations(remainingConversations);
        if (conversationId === activeConversationId) {
          Object.values(keepaliveTasksRef.current)
            .filter((task) => task.conversationId === activeConversationIdRef.current)
            .forEach((task) => {
              taskAbortControllersRef.current.get(task.taskId)?.abort();
              taskAbortControllersRef.current.delete(task.taskId);
              removeKeepaliveTask(task.taskId);
              aiGenerationClient.cancelTask(task.taskId).catch(() => {});
            });
          const currentIndex = conversations.findIndex((conversation) => conversation.id === conversationId);
          const nextConversation =
            remainingConversations[Math.max(0, currentIndex)] ||
            remainingConversations[Math.max(0, currentIndex - 1)] ||
            remainingConversations[0] ||
            null;

          if (nextConversation) {
            skipConversationAutoSelectRef.current = false;
            setActiveConversationId(nextConversation.id);
            activeConversationIdRef.current = nextConversation.id;
            persistActiveConversationId(nextConversation.id);
            setWorkspaceStarted(true);
            setComposerHidden(false);
            void loadConversation(nextConversation.id);
          } else {
            skipConversationAutoSelectRef.current = true;
            setActiveConversationId(null);
            activeConversationIdRef.current = null;
            persistActiveConversationId(null);
            messagesRef.current = [];
            setMessages([]);
            setInputValue("");
            setWorkspaceStarted(false);
            setComposerHidden(false);
            syncActiveGenerationUi(null);
          }
        }
        setProjectError(null);
        setDeleteDialog(null);
      } catch (error) {
        if (isAuthFailure(error)) {
          setProjectError(null);
          setDeleteDialog(null);
          return;
        }
        setProjectError(error instanceof Error ? error.message : String(error));
      } finally {
        setDeleteSubmitting(false);
      }
    },
    [activeConversationId, conversations, deleteDialog, deleteSubmitting, loadConversation, syncActiveGenerationUi],
  );

  const handleNewConversation = useCallback(() => {
    skipConversationAutoSelectRef.current = true;
    setActiveConversationId(null);
    persistActiveConversationId(null);
    messagesRef.current = [];
    pendingScrollToLatestRef.current = true;
    setMessages([]);
    setInputValue("");
    setWorkspaceStarted(false);
    setComposerHidden(false);
    syncActiveGenerationUi(null);
    activeConversationIdRef.current = null;
  }, [syncActiveGenerationUi]);

  useEffect(() => {
    if (resetToken === undefined || lastResetTokenRef.current === resetToken) return;
    lastResetTokenRef.current = resetToken;
    handleNewConversation();
  }, [handleNewConversation, resetToken]);

  const handleSelectProject = useCallback((id: string) => {
    if (!id) {
      handleNewConversation();
      return;
    }
    const conversationId = Number(id);
    if (Number.isFinite(conversationId)) {
      skipConversationAutoSelectRef.current = false;
      void loadConversation(conversationId);
    }
  }, [handleNewConversation, loadConversation]);

  const handleDeleteConversation = useCallback(async (id: number) => {
    try {
      await conversationClient.delete(id, { cleanupUserData: true });
      setConversations((prev) => prev.filter((c) => c.id !== id));
      if (activeConversationId === id) {
        setActiveConversationId(null);
        activeConversationIdRef.current = null;
        persistActiveConversationId(null);
        setMessages([]);
        setWorkspaceStarted(false);
        setComposerHidden(false);
        syncActiveGenerationUi(null);
      }
    } catch {}
  }, [activeConversationId, syncActiveGenerationUi]);

  const handleRenameConversation = useCallback(async (id: number, title: string) => {
    try {
      await conversationClient.update(id, { title });
      setConversations((prev) => prev.map((c) => c.id === id ? { ...c, title } : c));
    } catch {}
  }, []);

  useEffect(() => {
    referenceItemsRef.current = referenceItems;
  }, [referenceItems]);

  const focusPromptAt = useCallback((index: number) => {
    const nextIndex = Math.max(0, Math.min(inputValue.length, index));
    setCursorIndex(nextIndex);
    setPromptSelectionRange({ start: nextIndex, end: nextIndex });
    window.requestAnimationFrame(() => {
      textareaRef.current?.focus();
      textareaRef.current?.setSelectionRange(nextIndex, nextIndex);
    });
  }, [inputValue.length]);

  const normalizePromptSelection = useCallback(
    (selectionStart: number, selectionEnd: number) => {
      if (promptMentionTokenRanges.length === 0) {
        return { start: selectionStart, end: selectionEnd };
      }

      if (selectionStart === selectionEnd) {
        const range = findPromptMentionRangeInside(selectionStart, promptMentionTokenRanges);
        if (!range) return { start: selectionStart, end: selectionEnd };
        return { start: range.end, end: range.end };
      }

      const selectionMin = Math.min(selectionStart, selectionEnd);
      const selectionMax = Math.max(selectionStart, selectionEnd);
      const overlappedRange = findPromptMentionRangeOverlap(selectionMin, selectionMax, promptMentionTokenRanges);
      if (overlappedRange) {
        return { start: overlappedRange.end, end: overlappedRange.end };
      }

      const startRange = findPromptMentionRangeInside(selectionStart, promptMentionTokenRanges);
      const endRange = findPromptMentionRangeInside(selectionEnd, promptMentionTokenRanges);
      const nextStart = startRange ? startRange.start : selectionStart;
      const nextEnd = endRange ? endRange.end : selectionEnd;
      return { start: Math.min(nextStart, nextEnd), end: Math.max(nextStart, nextEnd) };
    },
    [promptMentionTokenRanges],
  );

  useEffect(
    () => () => {
      referenceItemsRef.current.forEach((item) => {
        disposeReferencePreview(item);
      });
    },
    [],
  );

  useEffect(() => {
    if (!toolbarMenuId) return undefined;

    const handlePointerDown = (event: PointerEvent) => {
      if (!toolbarRef.current?.contains(event.target as Node)) {
        setToolbarMenuId(null);
      }
    };

    window.addEventListener("pointerdown", handlePointerDown);
    return () => window.removeEventListener("pointerdown", handlePointerDown);
  }, [toolbarMenuId]);

  useEffect(() => {
    if (!referencePreviewOpen) return undefined;

    const handlePointerDown = (event: PointerEvent) => {
      if (!referenceRefsRef.current?.contains(event.target as Node)) {
        setReferencePreviewOpen(false);
      }
    };

    window.addEventListener("pointerdown", handlePointerDown);
    return () => window.removeEventListener("pointerdown", handlePointerDown);
  }, [referencePreviewOpen]);

  useEffect(() => {
    if (referenceItems.length === 0 && referencePreviewOpen) {
      setReferencePreviewOpen(false);
    }
  }, [referenceItems.length, referencePreviewOpen]);

  useEffect(() => {
    if (referenceItems.length === 0) {
      referenceTokenSequenceRef.current = {
        image: 0,
        video: 0,
        audio: 0,
        file: 0,
      };
    }
  }, [referenceItems.length]);

  useEffect(() => {
    if (referenceItems.length === 0) return;
    if (referenceItems.length < referenceLimit) return;
    setReferencePreviewOpen(true);
  }, [referenceItems.length, referenceLimit]);

  useEffect(() => {
    const el = textareaRef.current;
    if (!el) return;
    el.style.height = "auto";
    el.style.height = `${Math.min(Math.max(el.scrollHeight, 42), 120)}px`;
  }, [inputValue]);

  useEffect(() => {
    const surface = messagesSurfaceRef.current;
    if (!surface) return;
    const scrollDeltaThreshold = 8;
    const edgeThreshold = 3;

    const syncComposerVisibility = () => {
      const top = surface.scrollTop;
      const atTop = top <= edgeThreshold;
      const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
      shouldFollowNewMessagesRef.current = atBottom;
      setComposerHidden(!(atTop || atBottom));
      hideScrollActionHint();
      lastScrollTopRef.current = top;
    };

    lastScrollTopRef.current = surface.scrollTop;
    syncComposerVisibility();

    const handleScroll = () => {
      const top = surface.scrollTop;
      const delta = top - lastScrollTopRef.current;
      const atTop = top <= edgeThreshold;
      const atBottom = top + surface.clientHeight >= surface.scrollHeight - edgeThreshold;
      shouldFollowNewMessagesRef.current = atBottom;
      if (atTop || atBottom) {
        setComposerHidden(false);
        hideScrollActionHint();
      } else if (Math.abs(delta) > scrollDeltaThreshold) {
        setComposerHidden(true);
        showScrollActionHint(delta < 0 ? "top" : "bottom");
      }
      lastScrollTopRef.current = top;
    };

    surface.addEventListener("scroll", handleScroll, { passive: true });
    return () => surface.removeEventListener("scroll", handleScroll);
  }, [hasActivatedWorkspace, hideScrollActionHint, showScrollActionHint]);

  const scrollMessagesSurface = useCallback((direction: "top" | "bottom") => {
    const surface = messagesSurfaceRef.current;
    if (!surface) return;

    const top = direction === "top" ? 0 : surface.scrollHeight;
    hideScrollActionHint();
    setComposerHidden(false);
    surface.scrollTo({ top, behavior: "smooth" });
  }, [hideScrollActionHint]);

  const closeToolbarMenus = () => setToolbarMenuId(null);
  const toggleToolbarMenu = (menuId: Exclude<ToolbarMenuId, null>) => {
    setToolbarMenuId((current) => (current === menuId ? null : menuId));
  };

  // ── Onboarding tour helpers ────────────────────────────
  const obTarget = (map: Partial<Record<TourPhaseId, string>>): string | undefined =>
    effectiveOnboarding ? map[tourPhase] : undefined;

  const handleTourNext = useCallback((_phase: TourPhaseId, stepIndex: number) => {
    setTourStep(stepIndex);
  }, []);

  const handleTourSkip = useCallback((phase: TourPhaseId) => {
    const next: Record<TourPhaseId, TourPhaseId> = { chat: "image", image: "video", video: "video" };
    const nextPhase = next[phase];
    if (nextPhase === phase) {
      onEndOnboarding?.();
    } else {
      setTourPhase(nextPhase);
      setTourStep(0);
      if (nextPhase === "image") setActiveMode("image");
      else if (nextPhase === "video") setActiveMode("video");
    }
  }, [onEndOnboarding, setActiveMode]);

  const handleTourDone = useCallback(() => {
    setEffectiveOnboarding(false);
    onEndOnboarding?.();
  }, [onEndOnboarding]);

  // Advance tour phase when user switches mode during onboarding
  const handleModeChange = (mode: WorkbenchMode) => {
    if (effectiveOnboarding) {
      // Advance tour phase when switching to the next mode
      if (tourPhase === "chat" && mode === "image") { setTourPhase("image"); setTourStep(0); }
      else if (tourPhase === "image" && mode === "video") { setTourPhase("video"); setTourStep(0); }
      // Block switching to other modes during guided tour
      else if (mode !== tourPhase) return;
    }
    setActiveMode(mode);
    setToolbarMenuId(null);
    setReferencePreviewOpen(false);
    window.requestAnimationFrame(() => textareaRef.current?.focus());
  };

  const handlePromptChange = (event: ChangeEvent<HTMLTextAreaElement>) => {
    const selection = normalizePromptSelection(event.target.selectionStart, event.target.selectionEnd);
    setInputValue(event.target.value);
    setCursorIndex(selection.start);
    setPromptSelectionRange(selection);
    if (selection.start !== event.target.selectionStart || selection.end !== event.target.selectionEnd) {
      window.requestAnimationFrame(() => event.target.setSelectionRange(selection.start, selection.end));
    }
  };

  const handlePromptSelectionChange = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    const { selectionStart, selectionEnd } = event.currentTarget;
    const selection = normalizePromptSelection(selectionStart, selectionEnd);
    setCursorIndex(selection.start);
    setPromptSelectionRange(selection);
    if (selection.start !== selectionStart || selection.end !== selectionEnd) {
      const target = event.currentTarget;
      window.requestAnimationFrame(() => target.setSelectionRange(selection.start, selection.end));
    }
  };

  const handlePromptScroll = (event: SyntheticEvent<HTMLTextAreaElement>) => {
    const ta = event.currentTarget;
    const highlight = ta.parentElement?.querySelector<HTMLElement>(".wb-composer__highlight");
    if (highlight) highlight.scrollTop = ta.scrollTop;
  };

  const removeReferenceItem = (id: string) => {
    const target = referenceItems.find((item) => item.id === id);
    setReferenceItems((current) => {
      if (target) disposeReferencePreview(target);
      return current.filter((item) => item.id !== id);
    });
    if (target?.token) {
      const nextValue = removePromptMentionTokenFromText(inputValue, target.token);
      if (nextValue !== inputValue) {
        const nextCursor = Math.min(cursorIndex, nextValue.length);
        setInputValue(nextValue);
        setCursorIndex(nextCursor);
        setPromptSelectionRange({ start: nextCursor, end: nextCursor });
        window.requestAnimationFrame(() => {
          textareaRef.current?.setSelectionRange(nextCursor, nextCursor);
        });
      }
    }
  };

  const applyReferenceRemoteUrl = (id: string, remoteUrl: string) => {
    const nextItems = referenceItemsRef.current.map((item) =>
      item.id === id ? { ...item, remoteUrl } : item,
    );
    referenceItemsRef.current = nextItems;
    setReferenceItems(nextItems);
  };

  const handleReferenceUploadClick = () => {
    if (referenceItems.length > 0) {
      setToolbarMenuId(null);
      setReferencePreviewOpen((current) => !current);
      return;
    }
    referenceInputRef.current?.click();
  };

  const handleReferenceAddMore = () => {
    setToolbarMenuId(null);
    setReferencePreviewOpen(true);
    referenceInputRef.current?.click();
  };

  const processReferenceFiles = async (files: File[]) => {
    if (files.length === 0) return;

    const existingFingerprints = new Set(
      referenceItemsRef.current
        .map((item) => item.fingerprint)
        .filter((fingerprint): fingerprint is string => Boolean(fingerprint)),
    );
    const selectedFingerprints = new Set<string>();
    const preparedItems: Array<Omit<ReferenceItem, "id" | "token">> = [];

    for (const file of files) {
      try {
        const kind = inferReferenceKind(file, activeMode);
        const fingerprint = await buildReferenceFingerprint(file, kind);
        if (kind === "image") {
          if (existingFingerprints.has(fingerprint) || selectedFingerprints.has(fingerprint)) {
            continue;
          }
          selectedFingerprints.add(fingerprint);
        }

        const result = kind === "image" ? await compressReferenceImageIfNeeded(file) : { file, compressed: false };
        const canPreview = kind === "image" || kind === "video";
        preparedItems.push({
          kind,
          name: file.name,
          file: result.file,
          previewUrl: canPreview ? URL.createObjectURL(result.file) : undefined,
          fingerprint,
          originalSize: file.size,
          compressed: result.compressed,
        });
      } catch {
        continue;
      }
    }

    if (preparedItems.length > 0) {
      const currentItems = referenceItemsRef.current;
      const currentFingerprints = new Set(
        currentItems
          .map((item) => item.fingerprint)
          .filter((fingerprint): fingerprint is string => Boolean(fingerprint)),
      );
      const acceptedItems: ReferenceItem[] = [];
      const nextTokenSequence = { ...referenceTokenSequenceRef.current };

      preparedItems.forEach((item) => {
        if (currentItems.length + acceptedItems.length >= referenceLimit) {
          disposeReferencePreview(item);
          return;
        }
        if (item.kind === "image" && item.fingerprint && currentFingerprints.has(item.fingerprint)) {
          disposeReferencePreview(item);
          return;
        }
        if (item.fingerprint) currentFingerprints.add(item.fingerprint);
        const nextTokenIndex = nextTokenSequence[item.kind] + 1;
        nextTokenSequence[item.kind] = nextTokenIndex;
        acceptedItems.push({
          ...item,
          id: createId("ref"),
          token: buildReferenceToken(item.kind, nextTokenIndex),
        });
      });

      if (acceptedItems.length > 0) {
        referenceTokenSequenceRef.current = nextTokenSequence;
        const nextReferenceItems = [...currentItems, ...acceptedItems];
        referenceItemsRef.current = nextReferenceItems;
        setReferenceItems(nextReferenceItems);
        acceptedItems.forEach((item) => {
          if (item.file && (item.kind === "image" || item.kind === "video")) {
            void preUploadReference(item.file, item.name, item.fingerprint).then((url) => {
              if (url) applyReferenceRemoteUrl(item.id, url);
            });
          }
        });
      }
    }

    setToolbarMenuId(null);
    setReferencePreviewOpen(preparedItems.length > 0 || referenceItemsRef.current.length > 0);
    window.requestAnimationFrame(() => textareaRef.current?.focus());
  };

  const handleReferenceUpload = async (event: ChangeEvent<HTMLInputElement>) => {
    const files = Array.from(event.target.files || []);
    event.target.value = "";
    await processReferenceFiles(files);
  };

  const handleComposerDragEnter = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    composerDragCounterRef.current += 1;
    if (composerDragCounterRef.current === 1) {
      setIsComposerDragging(true);
    }
  }, []);

  const handleComposerDragLeave = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    composerDragCounterRef.current -= 1;
    if (composerDragCounterRef.current <= 0) {
      composerDragCounterRef.current = 0;
      setIsComposerDragging(false);
    }
  }, []);

  const handleComposerDragOver = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
  }, []);

  const handleComposerDrop = useCallback((e: React.DragEvent) => {
    e.preventDefault();
    e.stopPropagation();
    composerDragCounterRef.current = 0;
    setIsComposerDragging(false);
    const files = Array.from(e.dataTransfer.files);
    if (files.length > 0) {
      void processReferenceFiles(files);
    }
  }, [activeMode]);

  const insertPromptMention = (token: string) => {
    const rawBefore = inputValue.slice(0, cursorIndex);
    const after = inputValue.slice(cursorIndex);
    const match = detectMentionTrigger(rawBefore);
    const before = match
      ? rawBefore.slice(0, match.atIndex) + token
      : `${rawBefore}${rawBefore && !/\s$/.test(rawBefore) ? " " : ""}${token}`;
    const spacer = after.length === 0 || !/^\s/.test(after) ? " " : "";
    const nextValue = `${before}${spacer}${after}`;
    const nextCursor = before.length + spacer.length;
    setInputValue(nextValue);
    setCursorIndex(nextCursor);
    setPromptSelectionRange({ start: nextCursor, end: nextCursor });
    setMentionActiveIndex(0);
    setToolbarMenuId(null);
    setReferencePreviewOpen(false);

    const selectedOption = promptMentionOptions.find((o) => o.token === token);
    if (selectedOption && selectedOption.id.startsWith("asset-")) {
      const alreadyAdded = referenceItemsRef.current.some((r) => r.id === selectedOption.id);
      if (!alreadyAdded) {
        const newItem: ReferenceItem = {
          id: selectedOption.id,
          kind: selectedOption.kind as ReferenceKind,
          name: selectedOption.name,
          previewUrl: selectedOption.previewUrl,
          remoteUrl: selectedOption.remoteUrl,
          token: selectedOption.token,
        };
        const nextItems = [...referenceItemsRef.current, newItem];
        referenceItemsRef.current = nextItems;
        setReferenceItems(nextItems);
      }
    }

    window.requestAnimationFrame(() => {
      textareaRef.current?.focus();
      textareaRef.current?.setSelectionRange(nextCursor, nextCursor);
    });
  };

  const buildTaskPrompt = (trimmedPrompt: string) => {
    const specLines =
      activeMode === "image"
        ? [`模式：${MODE_META.image.label}`, `模型：${activeModel}`, `比例：${imageRatio}`, `清晰度：${imageQuality}`, `多宫格：${GRID_MODE_OPTIONS.find((item) => item.value === imageGridMode)?.label || imageGridMode}`]
        : activeMode === "video"
          ? [`模式：${MODE_META.video.label}`, `模型：${activeModel}`, `生成方式：${VIDEO_FRAME_OPTIONS.find((item) => item.value === videoFrameMode)?.label || videoFrameMode}`, `比例：${videoRatio}`, `时长：${videoDuration} 秒`, `清晰度：${videoQualityLabel}`]
          : [`模式：${MODE_META.chat.label}`];

    const referenceLines = referenceItems.map((item) => {
      const token = item.token;
      return `${token}：${item.name}`;
    });

    return [trimmedPrompt, "", ...specLines, ...(referenceLines.length ? ["", "参考素材：", ...referenceLines] : [])].join("\n");
  };

  const getCurrentSpecs = () => {
    if (activeMode === "image") {
      return [
        activeModel,
        imageSettingsSummary,
        GRID_MODE_OPTIONS.find((item) => item.value === imageGridMode)?.label || imageGridMode,
      ];
    }

    if (activeMode === "video") {
      return [
        activeModel,
        VIDEO_FRAME_OPTIONS.find((item) => item.value === videoFrameMode)?.label || videoFrameMode,
        `${videoRatio} / ${videoDuration}s / ${videoQualityLabel}`,
      ];
    }

    return ["工作流拆解", "脚本与分镜", "可复制节点"];
  };

  const updateAssistantMessage = (id: string, patch: Partial<ChatMessage>) => {
    const nextMessages = messagesRef.current.map((message) => {
      if (message.id !== id) return message;
      const nextPatch = { ...patch };
      if (
        typeof nextPatch.taskProgress === "number" &&
        typeof message.taskProgress === "number" &&
        nextPatch.taskProgress < message.taskProgress &&
        nextPatch.status !== "failed"
      ) {
        nextPatch.taskProgress = message.taskProgress;
      }
      return { ...message, ...nextPatch };
    });
    messagesRef.current = nextMessages;
    if (activeConversationIdRef.current) {
      rememberConversationMessages(activeConversationIdRef.current, nextMessages);
    }
    setMessages(nextMessages);
    return nextMessages;
  };

  const persistConversationAfterResult = (conversationId: number, nextMessages: ChatMessage[]) => {
    rememberConversationMessages(conversationId, nextMessages);
    void saveProjectMessages(nextMessages, "web-chat-result").catch(handleProjectSaveError);
  };

  const handleSendAction = async (promptOverride?: string) => {
    const trimmedPrompt = (promptOverride ?? inputValue).trim();
    if (!trimmedPrompt) return;
    const userKey = getGenerationUserKey(session?.user.id);
    if (activeMode !== "chat" && getActiveGenerationTaskCount(userKey) >= getEffectiveGenerationLimit()) return;
    setReferencePreviewOpen(false);

    let conversationId = activeConversationIdRef.current ?? activeConversationId;
    let baseMessages =
      conversationId && workspaceStarted
        ? messages
        : conversationId
          ? conversationMessagesCacheRef.current.get(conversationId) || []
          : [];
    const taskInput: CreatePreviewTaskInput = {
      title: `${MODE_META[activeMode].label} 创作任务`,
      type: MODE_META[activeMode].taskType,
      prompt: buildTaskPrompt(trimmedPrompt),
      params: activeMode === "image"
        ? { model: activeModelValue, ratio: imageRatio, quality: imageQuality, gridMode: imageGridMode }
        : activeMode === "video"
          ? {
              model: toHappyHorseDisplayModel(activeModelValue),
              ratio: videoRatio,
              quality: videoQuality,
              resolution: videoQuality,
              duration: Number(videoDuration),
              frameMode: videoFrameMode,
              muted: false,
              hasReferenceVideo: referenceItems.some((item) => item.kind === "video"),
            }
          : { model: activeModelValue },
    };

    if (activeMode === "image" && getCachedRole() === "admin") {
      console.log("[ai/workbench-image-submit]", {
        model: taskInput.params?.model,
        ratio: taskInput.params?.ratio,
        quality: taskInput.params?.quality,
        gridMode: taskInput.params?.gridMode,
        referenceCount: referenceItems.length,
        promptLength: taskInput.prompt.length,
        requestWillStart: isAuthenticated,
        authRequired: !isAuthenticated,
      });
    }

    if (!isAuthenticated) {
      onRequireLogin(taskInput);
      return;
    }

    if (conversationId && !conversationMessagesCacheRef.current.has(conversationId) && messages.length === 0) {
      try {
        const conversation = await conversationClient.get(conversationId);
        const restoredMessages = conversation.messages.filter((item): item is ChatMessage => {
          return (
            item &&
            typeof item.id === "string" &&
            (item.role === "user" || item.role === "assistant") &&
            typeof item.body === "string"
          );
        }) as ChatMessage[];
        baseMessages = restoredMessages;
        rememberConversationMessages(conversation.id, restoredMessages);
        setActiveConversationId(conversation.id);
        activeConversationIdRef.current = conversation.id;
      } catch {
        conversationId = null;
        activeConversationIdRef.current = null;
        setActiveConversationId(null);
        persistActiveConversationId(null);
        baseMessages = [];
      }
    }

    if (conversationId) {
      const conv = conversations.find((c) => c.id === conversationId);
      if (conv?.title === "New conversation" || conv?.title.startsWith("新对话")) {
        const newTitle = trimmedPrompt.slice(0, 20);
        conversationClient.update(conversationId, { title: newTitle }).catch((err) => {
          console.warn("[chat] conversation rename failed:", err?.message || err);
        });
        setConversations((prev) => prev.map((c) => c.id === conversationId ? { ...c, title: newTitle } : c));
      }
    }

    const attachments = buildChatAttachments(referenceItems);
    const messageCreatedAt = formatWorkbenchTimestamp();
    const userMessage: ChatMessage = {
      id: createId("user"),
      role: "user",
      author: "你",
      mode: activeMode,
      body: trimmedPrompt,
      prompt: trimmedPrompt,
      createdAt: messageCreatedAt,
      conversationId: conversationId || undefined,
      attachments,
    };
    const assistantMessageId = createId("assistant");
    const assistantMessage: ChatMessage = {
      id: assistantMessageId,
      role: "assistant",
      author: MODE_META[activeMode].label,
      mode: activeMode,
      prompt: trimmedPrompt,
      body: activeMode === "chat" ? "我先看一下上下文，马上接上。" : "正在读取当前模式、模型、规格和参考素材，准备创建生成任务。",
      createdAt: messageCreatedAt,
      status: "thinking",
      conversationId: conversationId || undefined,
      taskProgress: 28,
      taskStatusLabel: activeMode === "chat" ? "正在整理思路" : "Creating generation task",
    };

    setInputValue("");
    setPromptHistory((current) => [trimmedPrompt, ...current.filter((item) => item !== trimmedPrompt)].slice(0, 20));
    setWorkspaceStarted(true);
    setComposerHidden(false);
    const nextMessages = [...baseMessages, userMessage, assistantMessage];
    messagesRef.current = nextMessages;
    setMessages(nextMessages);
    if (conversationId) {
      rememberConversationMessages(conversationId, nextMessages);
    }
    setIsGenerating(true);
    setGenerationStatus("正在创建生成任务");
    setGenerationProgress(28);
    const abortController = new AbortController();
    generationAbortRef.current = abortController;
    let keepaliveStarted = false;
    let claimedGenerationSlotId: string | undefined;
    let releaseClaimedGenerationSlot: (() => void) | null = null;

    try {
      if (activeMode === "image" || activeMode === "video") {
        let taskId: string;
        const isVideoFilteredModel =
          activeMode === "video" && (isHappyHorseModel(taskInput.params?.model) || isViduModel(taskInput.params?.model) || isPixverseModel(taskInput.params?.model));
        const requestReferenceItems = isVideoFilteredModel
          ? referenceItems.filter((item) => item.kind === "image")
          : referenceItems;
        if (isVideoFilteredModel) {
          const droppedVideoCount = referenceItems.length - requestReferenceItems.length;
          if (droppedVideoCount > 0 && conversationId) {
            await patchConversationMessage(conversationId, userMessage.id, {
              body: `${userMessage.body}\n\n⚠ ${droppedVideoCount} 个视频参考素材已被忽略（当前模型仅支持图片参考）。`,
            });
          }
        }
        const refUrls = await resolveReferenceUrls(requestReferenceItems);
        const latestReferenceItems = [...referenceItemsRef.current];
        setReferenceItems(latestReferenceItems);
        const uploadedAttachments = buildChatAttachments(latestReferenceItems);
        if (uploadedAttachments.length) {
          const refreshedMessages = messagesRef.current.map((message) =>
            message.id === userMessage.id ? { ...message, attachments: uploadedAttachments } : message,
          );
          messagesRef.current = refreshedMessages;
          setMessages(refreshedMessages);
          if (conversationId) rememberConversationMessages(conversationId, refreshedMessages);
        }

        claimedGenerationSlotId = createId("workbench-generation");
        releaseClaimedGenerationSlot = claimGenerationSlot({
          userKey: getGenerationUserKey(session?.user.id),
          kind: activeMode,
          id: claimedGenerationSlotId,
        });

        if (activeMode === "image") {
          const result = await aiGenerationClient.createImageTask({
            conversationId: conversationId || undefined,
            model: taskInput.params?.model || fallbackImageModelValue,
            prompt: trimmedPrompt,
            ratio: taskInput.params?.ratio || imageRatio || "16:9",
            quality: taskInput.params?.quality || "1K",
            gridMode: taskInput.params?.gridMode || "single",
            referenceUrls: refUrls.length ? refUrls : undefined,
          });
          taskId = result.taskId;
          genTracker.submitTask({ title: trimmedPrompt.slice(0, 60), type: "image", status: "running", progress: 5, prompt: trimmedPrompt, sourceView: "workbench", taskId });
        } else {
          const requestModel = resolveVideoRequestModel({
            model: taskInput.params?.model || ENTERPRISE_DEFAULT_VIDEO_MODEL,
            referenceUrls: refUrls,
          });
          const result = await aiGenerationClient.createVideoTask({
            conversationId: conversationId || undefined,
            model: requestModel,
            prompt: trimmedPrompt,
            ratio: taskInput.params?.ratio || "16:9",
            duration: taskInput.params?.duration || 5,
            quality: taskInput.params?.quality || taskInput.params?.resolution || "1080P",
            resolution: taskInput.params?.resolution || taskInput.params?.quality || "1080P",
            frameMode: taskInput.params?.frameMode || "start-end",
            referenceUrls: refUrls.length ? refUrls : undefined,
            muted: taskInput.params?.muted ?? false,
            hasReferenceVideo: requestReferenceItems.some((item) => item.kind === "video"),
          });
          taskId = result.taskId;
          genTracker.submitTask({ title: trimmedPrompt.slice(0, 60), type: "video", status: "running", progress: 5, prompt: trimmedPrompt, sourceView: "workbench", taskId });
        }

        onRefreshUsage?.();

        if (!conversationId) {
          const taskSpecs = buildAssistantResult(activeMode, activeModel, getCurrentSpecs(), referenceItems.length);
          const submissionSourceMessages = messagesRef.current.length ? messagesRef.current : nextMessages;
          const submittedMessages = submissionSourceMessages.map((message) => {
            const base = { ...message, conversationId: 0 };
            if (message.id !== assistantMessageId) return base;
            return {
              ...base,
              body: "Task submitted, generating...",
              status: "thinking" as const,
              taskId,
              taskProgress: 30,
              taskStatusLabel: "Task submitted, generating...",
              resultType: activeMode,
              result: taskSpecs,
            };
          });
          const conv = await conversationClient.create(
            trimmedPrompt.slice(0, 20) || "新对话",
            activeMode,
            submittedMessages.map((message) => ({ ...message, conversationId: undefined })),
          );
          conversationId = conv.id;
          skipConversationAutoSelectRef.current = false;
          setActiveConversationId(conv.id);
          activeConversationIdRef.current = conv.id;
          setConversations((prev) => [conv, ...prev.filter((item) => item.id !== conv.id)]);

          const boundMessages = submittedMessages.map((message) => ({ ...message, conversationId: conv.id }));
          messagesRef.current = boundMessages;
          setMessages(boundMessages);
          rememberConversationMessages(conv.id, boundMessages);
          await aiGenerationClient.bindTaskToConversation(taskId, conv.id).catch((error) => {
            setProjectError(getErrorText(error));
          });
        }

        if (!conversationId) return;
        const keepaliveTask: WorkbenchKeepaliveTask = {
          taskId,
          conversationId,
          assistantMessageId,
          concurrencySlotId: claimedGenerationSlotId,
          mode: activeMode,
          modelLabel: activeModel,
          specs: getCurrentSpecs(),
          referenceCount: referenceItems.length,
          progress: 30,
          statusLabel: "Task submitted, generating...",
          startedAt: Date.now(),
        };
        const taskSpecs = buildAssistantResult(activeMode, activeModel, getCurrentSpecs(), referenceItems.length);
        keepaliveStarted = true;
        upsertKeepaliveTask(keepaliveTask);
        releaseClaimedGenerationSlot = null;
        await patchConversationMessage(conversationId, assistantMessageId, {
          body: "Task submitted, generating...",
          status: "thinking",
          taskId,
          conversationId,
          taskProgress: 30,
          taskStatusLabel: "Task submitted, generating...",
          resultType: activeMode,
          result: taskSpecs,
        });
        runKeepalivePoll(keepaliveTask);
      } else {
        let streamedText = "";
        let chatUsage: ChatMessage["taskUsage"] | undefined;
        setGenerationProgress(36);
        setGenerationStatus("正在回复");
        updateAssistantMessage(assistantMessageId, {
          body: "我在整理，马上说清楚。",
          status: "thinking",
          taskStatusLabel: "正在整理思路",
        });

        const historyMessages = buildNaturalChatHistoryMessages(baseMessages);

        await aiGenerationClient.streamChat(
          {
            model: activeModelValue,
            messages: [
              { role: "system", content: CHAT_NATURAL_SYSTEM_PROMPT },
              ...historyMessages,
              { role: "system", content: CHAT_TURN_STYLE_REMINDER },
              { role: "user", content: trimmedPrompt },
            ],
            temperature: 0.82,
          },
          (chunk) => {
            if (abortController.signal.aborted) return;
            streamedText += chunk;
            setGenerationProgress((current) => Math.min(95, current + 1));
            updateAssistantMessage(assistantMessageId, {
              body: streamedText || "我在整理，马上说清楚。",
              status: "thinking",
              taskStatusLabel: "正在回复",
            });
          },
          abortController.signal,
          (usage) => {
            chatUsage = usage;
          },
        );

        if (abortController.signal.aborted) return;
        setGenerationProgress(100);
        setGenerationStatus("回复完成");
        const completedMessages = updateAssistantMessage(assistantMessageId, {
          body: streamedText.trim() || "收到。你可以继续补充目标，我会顺着当前上下文往下拆。",
          status: "completed",
          taskUsage: chatUsage,
        });
        if (!conversationId) {
          const conv = await conversationClient.create(
            trimmedPrompt.slice(0, 20) || "新对话",
            activeMode,
            completedMessages.map((message) => ({ ...message, conversationId: undefined })),
          );
          conversationId = conv.id;
          skipConversationAutoSelectRef.current = false;
          setActiveConversationId(conv.id);
          activeConversationIdRef.current = conv.id;
          setConversations((prev) => [conv, ...prev.filter((item) => item.id !== conv.id)]);
          const boundMessages = completedMessages.map((message) => ({ ...message, conversationId: conv.id }));
          messagesRef.current = boundMessages;
          setMessages(boundMessages);
          rememberConversationMessages(conv.id, boundMessages);
          await conversationClient.update(conv.id, { messages: getPersistableMessages(boundMessages) as never[] });
          void loadConversations();
        } else {
          persistConversationAfterResult(conversationId, messagesRef.current);
        }
      }
    } catch (error) {
      if (abortController.signal.aborted) {
        setGenerationStatus("已停止");
        updateAssistantMessage(assistantMessageId, {
          body: "已停止当前生成。",
          status: "failed",
        });
        return;
      }
      setGenerationStatus("创建失败");
        if (isInsufficientBalance(error)) setShowRechargeModal(true);
        updateAssistantMessage(assistantMessageId, {
          body: isAuthFailure(error) ? "登录状态已失效，请重新登录后再继续。" : error instanceof Error ? error.message : "任务创建失败，请稍后重试。",
          status: "failed",
        });
    } finally {
      if (generationAbortRef.current === abortController) {
        generationAbortRef.current = null;
      }
      releaseClaimedGenerationSlot?.();
      if (keepaliveStarted) syncActiveGenerationUi(conversationId);
      else setIsGenerating(false);
    }
  };

  const handleStopAction = () => {
    const conversationId = activeConversationIdRef.current;
    const tasksToStop = Object.values(keepaliveTasksRef.current).filter(
      (task) => task.conversationId === conversationId,
    );
    tasksToStop.forEach((task) => {
      taskAbortControllersRef.current.get(task.taskId)?.abort();
      taskAbortControllersRef.current.delete(task.taskId);
      removeKeepaliveTask(task.taskId);
      aiGenerationClient.cancelTask(task.taskId).catch(() => {});
      void patchConversationMessage(task.conversationId, task.assistantMessageId, {
        body: "Stopped on this page.",
        status: "failed",
        taskStatusLabel: "Stopped",
      });
    });
    if (tasksToStop.length === 0) {
      generationAbortRef.current?.abort();
    }
    setIsGenerating(false);
    setGenerationStatus("Stopped");
    setGenerationProgress(0);
    setMessages((current) =>
      current.map((message) =>
        message.status === "thinking" && (!conversationId || message.conversationId === conversationId)
          ? {
              ...message,
              body: "Stopped on this page.",
              status: "failed",
              taskStatusLabel: "Stopped",
            }
          : message,
      ),
    );
  };

  const handleStopSingleTask = (messageId: string) => {
    const task = Object.values(keepaliveTasksRef.current).find(
      (t) => t.assistantMessageId === messageId,
    );
    if (task) {
      taskAbortControllersRef.current.get(task.taskId)?.abort();
      taskAbortControllersRef.current.delete(task.taskId);
      removeKeepaliveTask(task.taskId);
      aiGenerationClient.cancelTask(task.taskId).catch(() => {});
      void patchConversationMessage(task.conversationId, task.assistantMessageId, {
        body: "已终止",
        status: "failed",
        taskStatusLabel: "已终止",
      });
    } else {
      generationAbortRef.current?.abort();
    }
    setMessages((current) =>
      current.map((msg) =>
        msg.id === messageId
          ? { ...msg, body: "已终止", status: "failed" as const, taskStatusLabel: "已终止" }
          : msg,
      ),
    );
    const remainingTasks = Object.values(keepaliveTasksRef.current).filter(
      (t) => t.assistantMessageId !== messageId,
    );
    if (remainingTasks.length === 0) {
      setIsGenerating(false);
      setGenerationStatus("准备就绪");
      setGenerationProgress(0);
    }
  };

  const handleRegenerate = (message: ChatMessage) => {
    const userMsg = messages.find(
      (m, i) => m.role === "user" && messages[i + 1]?.id === message.id,
    );
    if (userMsg) {
      setInputValue(userMsg.body);
      void handleSendAction(userMsg.body);
    }
  };

  const handleReleaseStuckTask = (message: ChatMessage) => {
    if (message.taskId) {
      taskAbortControllersRef.current.get(message.taskId)?.abort();
      taskAbortControllersRef.current.delete(message.taskId);
      removeKeepaliveTask(message.taskId);
    }
    if (message.conversationId) {
      void patchConversationMessage(message.conversationId, message.id, {
        body: buildLocalTimeoutMessage(message.mode === "image" ? "image" : "video"),
        status: "local_timeout",
        taskLifecycleStatus: "local_timeout",
        taskRefundStatus: message.taskRefundStatus || "unknown",
        taskStatusLabel: "本地占用已释放",
      });
    }
    setMessages((current) =>
      current.map((item) =>
        item.id === message.id
          ? {
              ...item,
              body: buildLocalTimeoutMessage(item.mode === "image" ? "image" : "video"),
              status: "local_timeout",
              taskLifecycleStatus: "local_timeout",
              taskRefundStatus: item.taskRefundStatus || "unknown",
              taskStatusLabel: "本地占用已释放",
            }
          : item,
      ),
    );
    syncActiveGenerationUi();
  };

  const handleSuperResolveVideo = async (message: ChatMessage) => {
    if (!message.resultUrl || message.resultType !== "video") {
      setProjectError("仅支持对视频结果进行超分");
      return;
    }
    const userKey = getGenerationUserKey(session?.user.id);
    const activeCount = getActiveGenerationTaskCount(userKey);
    const limit = getEffectiveGenerationLimit();
    if (activeCount >= limit) {
      setProjectError(`当前已有 ${activeCount} 个任务进行中（上限${limit}个），请等待任一任务完成后再提交新任务`);
      return;
    }
    if (!isAuthenticated) {
      setProjectError("请先登录后再进行视频超分");
      return;
    }

    const conversationId = activeConversationIdRef.current || message.conversationId || activeConversationId;
    if (!conversationId) {
      setProjectError("请先在当前对话中保存记录后再进行视频超分");
      return;
    }

    const assistantMessageId = message.id;
    const previousResultUrl = message.resultUrl;
    const previousStatus = message.status || "completed";
    const previousTaskProgress = message.taskProgress;
    const previousTaskId = message.taskId;
    const previousResultOriginalUrl = message.resultOriginalUrl;
    const previousResultOssKey = message.resultOssKey;
    const previousResultMimeType = message.resultMimeType;
    const isAdmin = getCachedRole() === "admin";
    setIsGenerating(true);
    setGenerationStatus("正在提交超分任务");
    setGenerationProgress(12);

    if (isAdmin) console.log("[超分] 开始处理", { resultUrl: previousResultUrl, resultType: message.resultType, ossKey: message.resultOssKey, taskId: previousTaskId });

    let sourceResult: PersistedWorkbenchResultAsset;
    try {
      sourceResult = await persistWorkbenchResultAsset({
        title: message.result?.title || "生成视频",
        sourceUrl: previousResultUrl,
        resultType: "video",
        taskId: previousResultOssKey ? undefined : previousTaskId,
        originalUrl: previousResultOriginalUrl,
        existingOssKey: message.resultOssKey,
        mimeType: previousResultMimeType,
      });
      if (isAdmin) console.log("[超分] 资源转存成功", sourceResult);
    } catch (error) {
      if (isAdmin) console.error("[超分] 资源转存失败", error);
      setProjectError(error instanceof Error ? error.message : "视频资源转存失败，请重新生成后再试");
      setIsGenerating(false);
      setGenerationProgress(0);
      setGenerationStatus("超分失败");
      return;
    }

    await patchConversationMessage(conversationId, assistantMessageId, {
      body: "视频超分任务已提交，正在增强画质...",
      status: "thinking",
      conversationId,
      taskProgress: 12,
      taskStatusLabel: "正在提交超分任务",
      resultType: "video",
      resultUrl: sourceResult.url,
      resultOriginalUrl: sourceResult.originalUrl,
      resultOssKey: sourceResult.ossKey,
      resultMimeType: sourceResult.mimeType,
    });

    try {
      const superResolveVideoUrl = sourceResult.url;
      const result = await aiGenerationClient.createVideoSuperResolveTask({
        conversationId,
        videoUrl: superResolveVideoUrl,
      });
      if (isAdmin) console.log("[超分] API 提交成功", { taskId: result.taskId, videoUrl: superResolveVideoUrl });
      const keepaliveTask: WorkbenchKeepaliveTask = {
        taskId: result.taskId,
        conversationId,
        assistantMessageId,
        operation: "video-super-resolution",
        mode: "video",
        modelLabel: "视频超分",
        specs: ["2x", "MP4"],
        referenceCount: 0,
        progress: 18,
        statusLabel: "超分处理中...",
        startedAt: Date.now(),
      };
      upsertKeepaliveTask(keepaliveTask);
      await patchConversationMessage(conversationId, assistantMessageId, {
        taskId: result.taskId,
        status: "thinking",
        taskProgress: 18,
        taskStatusLabel: "超分处理中...",
        resultUrl: sourceResult.url,
        resultOriginalUrl: sourceResult.originalUrl,
        resultOssKey: sourceResult.ossKey,
        resultMimeType: sourceResult.mimeType,
      });
      runKeepalivePoll(keepaliveTask);
    } catch (error) {
      if (isAdmin) console.error("[超分] API 提交失败", error, { videoUrl: sourceResult.url, conversationId });
      if (isInsufficientBalance(error)) setShowRechargeModal(true);
      await patchConversationMessage(conversationId, assistantMessageId, {
        body: error instanceof Error ? error.message : "视频超分任务提交失败",
        status: previousStatus,
        taskId: previousTaskId,
        taskProgress: previousTaskProgress,
        taskStatusLabel: "超分失败",
        resultUrl: sourceResult.url,
        resultType: "video",
        resultOriginalUrl: sourceResult.originalUrl,
        resultOssKey: sourceResult.ossKey,
        resultMimeType: sourceResult.mimeType,
      });
      setIsGenerating(false);
      setGenerationProgress(0);
      setGenerationStatus("超分失败");
    }
  };

  const handleSuperResolveImage = async (message: ChatMessage) => {
    if (!message.resultUrl || message.resultType === "video") {
      setProjectError("仅支持对图片结果进行超分");
      return;
    }
    const userKey = getGenerationUserKey(session?.user.id);
    const activeCount = getActiveGenerationTaskCount(userKey);
    const limit = getEffectiveGenerationLimit();
    if (activeCount >= limit) {
      setProjectError(`当前已有 ${activeCount} 个任务进行中（上限${limit}个），请等待任一任务完成后再提交新任务`);
      return;
    }
    if (!isAuthenticated) {
      setProjectError("请先登录后再进行图片超分");
      return;
    }

    const conversationId = activeConversationIdRef.current || message.conversationId || activeConversationId;
    if (!conversationId) {
      setProjectError("请先在当前对话中保存记录后再进行图片超分");
      return;
    }

    const assistantMessageId = message.id;
    const previousResultUrl = message.resultUrl;
    const previousStatus = message.status || "completed";
    const previousTaskProgress = message.taskProgress;
    const previousTaskId = message.taskId;
    const isAdmin = getCachedRole() === "admin";

    setIsGenerating(true);
    setGenerationStatus("正在提交图片超分任务");
    setGenerationProgress(12);

    const imageUrl = message.resultOriginalUrl || message.resultUrl;
    if (isAdmin) console.log("[图片超分] 开始处理", { imageUrl });

    await patchConversationMessage(conversationId, assistantMessageId, {
      body: "图片超分任务已提交，正在增强画质...",
      status: "thinking",
      conversationId,
      taskProgress: 12,
      taskStatusLabel: "正在提交超分任务",
      resultType: "image",
      resultUrl: previousResultUrl,
    });

    try {
      const result = await aiGenerationClient.createImageSuperResolveTask({
        conversationId,
        imageUrl,
        scale: 2,
      });
      if (isAdmin) console.log("[图片超分] API 提交成功", { taskId: result.taskId });

      const keepaliveTask: WorkbenchKeepaliveTask = {
        taskId: result.taskId,
        conversationId,
        assistantMessageId,
        operation: "video-super-resolution",
        mode: "image",
        modelLabel: "图片超分",
        specs: ["2x"],
        referenceCount: 0,
        progress: 18,
        statusLabel: "超分处理中...",
        startedAt: Date.now(),
      };
      upsertKeepaliveTask(keepaliveTask);
      await patchConversationMessage(conversationId, assistantMessageId, {
        taskId: result.taskId,
        status: "thinking",
        taskProgress: 18,
        taskStatusLabel: "超分处理中...",
        resultUrl: previousResultUrl,
        resultType: "image",
      });
      runKeepalivePoll(keepaliveTask);
    } catch (error) {
      if (isAdmin) console.error("[图片超分] API 提交失败", error);
      if (isInsufficientBalance(error)) setShowRechargeModal(true);
      setProjectError(error instanceof Error ? error.message : "图片超分提交失败");
      await patchConversationMessage(conversationId, assistantMessageId, {
        body: "图片超分失败",
        status: previousStatus,
        taskId: previousTaskId,
        taskProgress: previousTaskProgress,
        taskStatusLabel: "超分失败",
        resultUrl: previousResultUrl,
        resultType: "image",
      });
      setIsGenerating(false);
      setGenerationProgress(0);
      setGenerationStatus("超分失败");
    }
  };

  const handlePromptKeyDown = (event: KeyboardEvent<HTMLTextAreaElement>) => {
    if (promptMentionOpen && promptMentionOptions.length > 0) {
      if (event.key === "ArrowDown") {
        event.preventDefault();
        setMentionActiveIndex((current) => (current + 1) % promptMentionOptions.length);
        return;
      }

      if (event.key === "ArrowUp") {
        event.preventDefault();
        setMentionActiveIndex((current) => (current - 1 + promptMentionOptions.length) % promptMentionOptions.length);
        return;
      }

      if (event.key === "Enter" || event.key === "Tab") {
        event.preventDefault();
        insertPromptMention(promptMentionOptions[mentionActiveIndex]?.token || promptMentionOptions[0].token);
        return;
      }
    }

    const { selectionStart, selectionEnd } = event.currentTarget;
    const selectionMin = Math.min(selectionStart, selectionEnd);
    const selectionMax = Math.max(selectionStart, selectionEnd);
    const selectedMentionRanges = promptMentionTokenRanges.filter(
      (range) => selectionMin < range.end && selectionMax > range.start,
    );

    const getDeletionRange = () => {
      if (selectionStart !== selectionEnd) {
        if (selectedMentionRanges.length === 0) return null;
        return {
          start: selectedMentionRanges.reduce((min, range) => Math.min(min, range.start), selectionMin),
          end: selectedMentionRanges.reduce((max, range) => Math.max(max, range.end), selectionMax),
        };
      }

      if (event.key === "Backspace") {
        return (
          promptMentionTokenRanges.find(
            (range) => selectionStart === range.end || (selectionStart > range.start && selectionStart < range.end),
          ) || null
        );
      }

      if (event.key === "Delete") {
        return (
          promptMentionTokenRanges.find(
            (range) => selectionStart === range.start || (selectionStart > range.start && selectionStart < range.end),
          ) || null
        );
      }

      return null;
    };

    const deletionRange = event.key === "Backspace" || event.key === "Delete" ? getDeletionRange() : null;
    if (deletionRange) {
      event.preventDefault();
      let start = deletionRange.start;
      let end = deletionRange.end;
      if (start > 0 && inputValue[start - 1] === " ") {
        start -= 1;
      } else if (end < inputValue.length && inputValue[end] === " ") {
        end += 1;
      }

      const nextValue = removePromptTextRange(inputValue, start, end);
      const nextCursor = Math.min(start, nextValue.length);
      setInputValue(nextValue);
      setCursorIndex(nextCursor);
      setPromptSelectionRange({ start: nextCursor, end: nextCursor });
      window.requestAnimationFrame(() => {
        textareaRef.current?.setSelectionRange(nextCursor, nextCursor);
      });
      return;
    }

    if ((event.metaKey || event.ctrlKey) && event.key === "Enter") {
      event.preventDefault();
      void handleSendAction();
    }
  };

  const activeGenerationCount = getActiveGenerationTaskCount(getGenerationUserKey(session?.user.id));
  const activeGenerationLimit = getEffectiveGenerationLimit();
  const generationLimitReached = activeMode !== "chat" && activeGenerationCount >= activeGenerationLimit;
  const promptIsEmpty = !inputValue.trim();
  const sendDisabled = promptIsEmpty || generationLimitReached;
  const sendButtonTitle = promptIsEmpty
    ? "输入内容后可发送"
    : generationLimitReached
      ? `当前已有 ${activeGenerationCount} 个任务进行中（上限 ${activeGenerationLimit} 个），请等待任一任务完成`
      : billingEstimate.title;

  const suggestedPrompts = [
    { text: "画一个赛博朋克风格的城市夜景", mode: "image" as WorkbenchMode },
    { text: "生成一段雨中漫步的电影镜头", mode: "video" as WorkbenchMode },
    { text: "帮我设计一个科幻短片的分镜脚本", mode: "chat" as WorkbenchMode },
    { text: "画一组动漫角色定妆照", mode: "image" as WorkbenchMode },
  ];

  const handleSuggestedPrompt = (prompt: string, mode: WorkbenchMode) => {
    setActiveMode(mode);
    setInputValue(prompt);
    window.requestAnimationFrame(() => textareaRef.current?.focus());
  };

  const handleUsePromptCase = (item: PromptCaseViewModel) => {
    setActiveMode("image");
    setInputValue(item.prompt);
    setSelectedPromptCase(null);
    window.requestAnimationFrame(() => textareaRef.current?.focus());
  };

  const handleCopyPromptCase = async (item: PromptCaseViewModel) => {
    await navigator.clipboard?.writeText(item.prompt);
  };

  const renderConversationSidebar = () => (
    <ProjectSidebar
      projects={conversationRecords}
      activeId={activeConversationId ? String(activeConversationId) : null}
      collapsed={sidebarCollapsed}
      loading={false}
      error={projectError}
      onToggle={() => setSidebarCollapsed((v) => (hasSidebarRecords ? !v : true))}
      onSelect={handleSelectProject}
      onRefresh={handleRefreshProject}
      onRename={handleRenameProject}
      onDelete={requestDeleteProject}
    />
  );

  const renderDeleteDialog = () =>
    deleteDialog ? (
      <div className="workbench-delete-modal" role="dialog" aria-modal="true" aria-labelledby="workbench-delete-title">
        <button
          type="button"
          className="workbench-delete-modal__backdrop"
          aria-label="Cancel delete"
          onClick={() => {
            if (!deleteSubmitting) setDeleteDialog(null);
          }}
        />
        <div className="workbench-delete-modal__panel">
          <div className="workbench-delete-modal__icon">
            <DeleteOutlined />
          </div>
          <div className="workbench-delete-modal__copy">
            <span>删除对话记录</span>
            <h2 id="workbench-delete-title">{deleteDialog.title}</h2>
            <p>删除后这条首页任务记录会从当前账号移除，已生成的线上资源不会被这个操作清空。</p>
          </div>
          <div className="workbench-delete-modal__actions">
            <button type="button" onClick={() => setDeleteDialog(null)} disabled={deleteSubmitting}>
              取消
            </button>
            <button type="button" className="is-danger" onClick={() => void confirmDeleteProject()} disabled={deleteSubmitting}>
              {deleteSubmitting ? "删除中..." : "确认删除"}
            </button>
          </div>
        </div>
      </div>
    ) : null;

  const renderMentionPanel = () => {
    if (!promptMentionOpen) return null;
    const refCount = referenceItems.length;
    const assetCount = promptMentionOptions.length - refCount;
    const hasBoth = refCount > 0 && assetCount > 0;
    return (
      <div
        className={`ai-chat-mention-panel ai-chat-mention-panel--${mentionPanelPlacement}`}
        role="listbox"
        aria-label="选择参考内容"
      >
        <div className="ai-chat-mention-header">引用参考内容</div>
        {promptMentionOptions.length > 0 ? (
          <div className="ai-chat-mention-list">
            {promptMentionOptions.flatMap((item, index) => {
              const nodes: React.ReactNode[] = [];
              if (hasBoth && index === 0) nodes.push(<div key="section-upload" className="ai-chat-mention-section">本次上传</div>);
              if (hasBoth && index === refCount) nodes.push(<div key="section-assets" className="ai-chat-mention-section">资产库</div>);
              nodes.push(
                <button
                  key={item.id}
                  type="button"
                  role="option"
                  aria-selected={index === mentionActiveIndex}
                  className={`ai-chat-mention-item${index === mentionActiveIndex ? " is-active" : ""}`}
                  onMouseEnter={() => setMentionActiveIndex(index)}
                  onClick={() => insertPromptMention(item.token)}
                >
                  <span className="ai-chat-mention-thumb">
                    <ReferencePreview item={item} />
                  </span>
                  <span className="ai-chat-mention-label">{item.name}</span>
                  <span className="ai-chat-mention-token">{item.token}</span>
                </button>,
              );
              return nodes;
            })}
          </div>
        ) : (
          <div className="ai-chat-mention-empty">上传参考内容或保存结果到资产库后，用 @ 引用。</div>
        )}
      </div>
    );
  };

  const renderComposerReferences = (disabled = false) => (
    <div
      ref={referenceRefsRef}
      className={`wb-composer__refs${referenceItems.length > 0 ? " has-items" : ""}${referencePreviewOpen ? " is-open" : ""}`}
    >
      <button
        type="button"
        className="wb-composer__ref-upload"
        onClick={handleReferenceUploadClick}
        disabled={disabled}
        data-onboarding={obTarget({ chat: "onboarding-chat-upload", image: "onboarding-image-upload", video: "onboarding-video-upload" })}
        aria-label={`上传${referenceUploadLabel}`}
        aria-expanded={referenceItems.length > 0 ? referencePreviewOpen : undefined}
        aria-controls={referenceItems.length > 0 ? "workbench-reference-stack" : undefined}
      >
        <PlusOutlined />
        <span className="wb-composer__ref-label">{referenceButtonLabel}</span>
        {referenceItems.length > 0 ? <span className="wb-composer__ref-count">{referenceItems.length}/{referenceLimit}</span> : null}
      </button>
      {referenceItems.length > 0 && referencePreviewOpen ? (
        <div id="workbench-reference-stack" className="wb-composer__ref-stack" aria-label="已上传参考内容">
          {referenceItems.map((item) => (
            <div
              key={item.id}
              className="wb-composer__ref-card"
              title={`${item.token} ${item.name}`}
            >
              <button
                type="button"
                className="wb-composer__ref-preview"
                disabled={disabled}
                aria-label={`${item.token} ${item.name}`}
                onClick={() => insertPromptMention(item.token)}
              >
                <ReferencePreview item={item} label={getReferenceKindLabel(item.kind)} />
              </button>
              {(item.kind === "image" || item.kind === "video") && item.previewUrl ? (
                <span className="wb-composer__ref-zoom" aria-hidden="true">
                  {item.kind === "video" ? <video src={item.previewUrl} muted playsInline /> : <img src={item.previewUrl} alt="" />}
                </span>
              ) : null}
              <button
                type="button"
                className="wb-composer__ref-remove"
                aria-label={`移除${item.name}`}
                disabled={disabled}
                onClick={() => removeReferenceItem(item.id)}
              >
                <CloseOutlined />
              </button>
            </div>
          ))}
          {!disabled && referenceItems.length < referenceLimit ? (
            <button
              type="button"
              className="wb-composer__ref-add-more"
              onClick={handleReferenceAddMore}
              aria-label={`继续上传${referenceUploadLabel}`}
            >
              <PlusOutlined />
            </button>
          ) : null}
        </div>
      ) : null}
    </div>
  );

  const renderComposerToolbar = (disabled = false, _showStop = false) => (
    <div className="wb-composer__toolbar">
      <div className="wb-composer__toolbar-left">
        <span data-onboarding={obTarget({ chat: "onboarding-mode-selector", image: "onboarding-mode-selector" })}>
        <SelectChip
          chipId="studio-mode"
          value={activeMode}
          options={MODE_OPTIONS}
          disabled={disabled}
          isOpen={toolbarMenuId === "studio-mode"}
          onToggle={() => toggleToolbarMenu("studio-mode")}
          onClose={closeToolbarMenus}
          onChange={(value) => handleModeChange(value as WorkbenchMode)}
          ariaLabel="工作台模式"
          direction={dropdownDirection}
        />
        </span>
        {activeMode === "chat" && (
          <>
            <span data-onboarding={obTarget({ chat: "onboarding-chat-model" })}>
            <SelectChip
              chipId="chat-model"
              value={chatModel}
              options={CHAT_MODEL_OPTIONS}
              disabled={disabled}
              isOpen={toolbarMenuId === "chat-model"}
              onToggle={() => toggleToolbarMenu("chat-model")}
              onClose={closeToolbarMenus}
              onChange={setChatModel}
              ariaLabel="对话模型"
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ chat: "onboarding-chat-speed" })}>
            <SelectChip
              chipId="chat-speed"
              value={thinkingSpeed}
              options={THINKING_SPEED_OPTIONS}
              disabled={disabled}
              isOpen={toolbarMenuId === "chat-speed"}
              onToggle={() => toggleToolbarMenu("chat-speed")}
              onClose={closeToolbarMenus}
              onChange={setThinkingSpeed}
              ariaLabel="思考速度"
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ chat: "onboarding-chat-depth" })}>
            <SelectChip
              chipId="chat-depth"
              value={thinkingDepth}
              options={THINKING_DEPTH_OPTIONS}
              disabled={disabled}
              isOpen={toolbarMenuId === "chat-depth"}
              onToggle={() => toggleToolbarMenu("chat-depth")}
              onClose={closeToolbarMenus}
              onChange={setThinkingDepth}
              ariaLabel="思考深度"
              direction={dropdownDirection}
            />
            </span>
          </>
        )}
        {activeMode === "image" && (
          <>
            <span data-onboarding={obTarget({ image: "onboarding-image-model" })}>
            <SelectChip
              chipId="image-model"
              value={imageModel}
              options={imageModelOptions}
              disabled={disabled}
              isOpen={toolbarMenuId === "image-model"}
              onToggle={() => toggleToolbarMenu("image-model")}
              onClose={closeToolbarMenus}
              onChange={(v) => { setImageModel(v); if (!GRID_SUPPORTED_MODELS.has(v)) setImageGridMode("single"); }}
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ image: "onboarding-image-settings" })}>
            <CompoundSelectChip
              chipId="image-settings"
              summary={imageSettingsSummary}
              groups={imageSettingGroups}
              disabled={disabled}
              isOpen={toolbarMenuId === "image-settings"}
              onToggle={() => toggleToolbarMenu("image-settings")}
              direction={dropdownDirection}
            />
            </span>
            {GRID_SUPPORTED_MODELS.has(imageModel) && (
            <span data-onboarding={obTarget({ image: "onboarding-image-grid" })}>
            <SelectChip
              chipId="image-grid-mode"
              value={imageGridMode}
              options={imageGridModeOptions}
              disabled={disabled}
              isOpen={toolbarMenuId === "image-grid-mode"}
              onToggle={() => toggleToolbarMenu("image-grid-mode")}
              onClose={closeToolbarMenus}
              onChange={setImageGridMode}
              direction={dropdownDirection}
            />
            </span>
            )}
          </>
        )}
        {activeMode === "video" && (
          <>
            <span data-onboarding={obTarget({ video: "onboarding-video-model" })}>
            <SelectChip
              chipId="video-model"
              value={videoModel}
              options={videoModelOptions}
              disabled={disabled}
              isOpen={toolbarMenuId === "video-model"}
              onToggle={() => toggleToolbarMenu("video-model")}
              onClose={closeToolbarMenus}
              onChange={setVideoModel}
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ video: "onboarding-video-frame" })}>
            <SelectChip
              chipId="video-mode"
              value={videoFrameMode}
              options={VIDEO_FRAME_OPTIONS}
              disabled={disabled}
              isOpen={toolbarMenuId === "video-mode"}
              onToggle={() => toggleToolbarMenu("video-mode")}
              onClose={closeToolbarMenus}
              onChange={setVideoFrameMode}
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ video: "onboarding-video-ratio" })}>
            <CompoundSelectChip
              chipId="video-ratio"
              summary={videoRatio}
              groups={videoRatioGroups}
              disabled={disabled}
              isOpen={toolbarMenuId === "video-ratio"}
              onToggle={() => toggleToolbarMenu("video-ratio")}
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ video: "onboarding-video-duration" })}>
            <InlineOptionChip
              chipId="video-duration"
              value={videoDuration}
              options={VIDEO_DURATION_OPTIONS}
              icon={<ClockCircleOutlined />}
              disabled={disabled}
              isOpen={toolbarMenuId === "video-duration"}
              onToggle={() => toggleToolbarMenu("video-duration")}
              onClose={closeToolbarMenus}
              onChange={setVideoDuration}
              direction={dropdownDirection}
            />
            </span>
            <span data-onboarding={obTarget({ video: "onboarding-video-quality" })}>
            <InlineOptionChip
              chipId="video-quality"
              value={videoQuality}
              options={videoQualityOptions}
              icon={<SettingOutlined />}
              disabled={disabled}
              isOpen={toolbarMenuId === "video-quality"}
              onToggle={() => toggleToolbarMenu("video-quality")}
              onClose={closeToolbarMenus}
              onChange={setVideoQuality}
              direction={dropdownDirection}
            />
            </span>
          </>
        )}
      </div>
      <div className="wb-composer__toolbar-right">
          <span className="wb-composer__billing-estimate" title={billingEstimate.title}>
            {billingEstimate.label}
          </span>
          <button
            type="button"
            className={`wb-composer__send-primary${isGenerating ? " is-loading" : ""}`}
            disabled={sendDisabled || isGenerating}
            title={isGenerating ? "任务处理中" : sendButtonTitle}
            aria-label={isGenerating ? "任务处理中" : sendButtonTitle}
            data-onboarding={obTarget({ video: "onboarding-video-generate" })}
            onClick={() => {
              if (getCachedRole() === "admin") console.log("[ai/workbench-send-click]", {
                mode: activeMode,
                model: activeModelValue,
                inputLength: inputValue.trim().length,
                isAuthenticated,
                sendDisabled,
                isGenerating,
              });
              void handleSendAction();
            }}
            data-smoke-id="ai-chat-send"
          >
            {isGenerating ? <LoadingOutlined /> : <SendOutlined />}
          </button>
      </div>
    </div>
  );

  // ─── LAUNCH STATE (homepage) ───
  const renderMessagePreviewOverlay = () =>
    messagePreviewAttachment?.previewUrl ? (
      <div className="ai-chat-media-preview" role="dialog" aria-modal="true" onClick={() => setMessagePreviewAttachment(null)}>
        <div
          className={`ai-chat-media-preview__panel${messagePreviewAttachment.kind === "video" ? " is-video" : ""}`}
          onClick={(event) => event.stopPropagation()}
        >
          <div className="ai-chat-media-preview__head">
            <span>{messagePreviewAttachment.name}</span>
            <button type="button" onClick={() => setMessagePreviewAttachment(null)} aria-label="Close preview">
              <CloseOutlined />
            </button>
          </div>
          <div className="ai-chat-media-preview__body">
            {messagePreviewAttachment.kind === "video" ? (
              <ImmersiveVideoPlayer
                src={messagePreviewAttachment.previewUrl}
                title={messagePreviewAttachment.name}
              />
            ) : (
              <img src={messagePreviewAttachment.previewUrl} alt={messagePreviewAttachment.name} />
            )}
          </div>
        </div>
      </div>
    ) : null;

  const renderPromptCaseOverlay = () => {
    if (!selectedPromptCase) return null;

    const measuredRatio = promptCaseMeasuredRatios[selectedPromptCase.id];
    const ratioParts = selectedPromptCase.ratio.replace(/\s+/g, "").split(":").map(Number);
    const declaredRatio =
      ratioParts.length === 2 && ratioParts[0] > 0 && ratioParts[1] > 0
        ? ratioParts[0] / ratioParts[1]
        : null;
    const caseRatio =
      typeof measuredRatio === "number" && Number.isFinite(measuredRatio) && measuredRatio > 0
        ? measuredRatio
        : declaredRatio;
    const copyLength = `${selectedPromptCase.summary} ${selectedPromptCase.prompt}`.length;
    const modalClassName = [
      "wb-prompt-case-modal",
      caseRatio && caseRatio < 0.72 ? "is-tall-media" : "",
      caseRatio && caseRatio >= 0.72 && caseRatio < 1 ? "is-portrait-media" : "",
      copyLength > 260 ? "is-long-copy" : "",
    ].filter(Boolean).join(" ");

    return (
      <div className={modalClassName} role="dialog" aria-modal="true" aria-labelledby="wb-prompt-case-title">
        <button
          type="button"
          className="wb-prompt-case-modal__backdrop"
          aria-label="关闭案例详情"
          onClick={() => setSelectedPromptCase(null)}
        />
        <section className="wb-prompt-case-modal__panel">
          <div className="wb-prompt-case-modal__media">
            <img
              src={selectedPromptCase.imageUrl}
              alt={selectedPromptCase.title}
              onLoad={(event) => handlePromptCaseImageLoad(selectedPromptCase.id, event)}
            />
          </div>
          <aside className="wb-prompt-case-modal__sidebar">
            <button
              type="button"
              className="wb-prompt-case-modal__close"
              aria-label="关闭案例详情"
              onClick={() => setSelectedPromptCase(null)}
            >
              <CloseOutlined />
            </button>
            <div className="wb-prompt-case-author">
              <span>{selectedPromptCase.author.slice(0, 1).toUpperCase()}</span>
              <div>
                <strong>{selectedPromptCase.author}</strong>
                <em>{selectedPromptCase.category}</em>
              </div>
            </div>
            <div className="wb-prompt-case-meta">
              <h2 id="wb-prompt-case-title">{selectedPromptCase.title}</h2>
              <p>{selectedPromptCase.summary}</p>
              <span>{selectedPromptCase.ratio} · 图片案例</span>
            </div>
            <div className="wb-prompt-case-prompt">
              <span>图片提示词</span>
              <p>{selectedPromptCase.prompt}</p>
            </div>
            <div className="wb-prompt-case-actions">
              <button type="button" onClick={() => handleUsePromptCase(selectedPromptCase)}>
                <PictureOutlined />
                套用提示词
              </button>
              <button type="button" onClick={() => void handleCopyPromptCase(selectedPromptCase)}>
                <CopyOutlined />
                复制提示词
              </button>
            </div>
          </aside>
        </section>
      </div>
    );
  };

  if (!hasActivatedWorkspace) {
    return (
      <section
        className={`ai-workbench-page is-launch mode-${activeMode} page-motion`}
        style={themeVars}
      >
        <input ref={referenceInputRef} type="file" accept={getReferenceAccept(activeMode, videoFrameMode)} multiple hidden onChange={handleReferenceUpload} />
        <div className="ai-workbench-shell">
          <div className="ai-workbench-main">
        <div className="wb-home">
        <div className="wb-home__glow" />

          <div className="wb-home__hero">
            <h1 className="wb-home__title">今天想生成什么？</h1>
          </div>

          <div
            className={`wb-home__composer${isComposerDragging ? " wb-composer--drag-active" : ""}`}
            ref={toolbarRef}
            onDragEnter={handleComposerDragEnter}
            onDragLeave={handleComposerDragLeave}
            onDragOver={handleComposerDragOver}
            onDrop={handleComposerDrop}
          >
            <div className="wb-composer__content">
              <div className="wb-composer__input-row">
                {renderComposerReferences(false)}
                <div className="wb-composer__main">
                  <textarea
                    ref={textareaRef}
                    className={`wb-composer__textarea${showPromptPreview ? " wb-composer__textarea--overlay-mode" : ""}`}
                    placeholder={composerPlaceholder}
                    value={inputValue}
                    data-onboarding={obTarget({ chat: "onboarding-chat-input", image: "onboarding-image-input" })}
                    onChange={handlePromptChange}
                    onSelect={handlePromptSelectionChange}
                    onKeyUp={handlePromptSelectionChange}
                    onClick={handlePromptSelectionChange}
                    onMouseUp={handlePromptSelectionChange}
                    onKeyDown={handlePromptKeyDown}
                    onScroll={handlePromptScroll}
                    data-smoke-id="ai-workbench-prompt"
                  />
                  {showPromptPreview ? (
                    <PromptPreviewLayer text={inputValue} items={promptMentionOptions} onTokenPointerDown={focusPromptAt} />
                  ) : null}
                  {renderMentionPanel()}
                </div>
              </div>
              {renderComposerToolbar(false, false)}
            </div>
          </div>

          <div className="wb-home__suggestions">
            {suggestedPrompts.map((item) => (
              <button key={item.text} type="button" className="wb-suggestion-chip" onClick={() => handleSuggestedPrompt(item.text, item.mode)}>
                <span className="wb-suggestion-chip__icon">{MODE_ICONS[item.mode]}</span>
                <span>{item.text}</span>
              </button>
            ))}
          </div>

          <section className="wb-prompt-cases" aria-label="图片提示词案例">
            <div className="wb-showcase__header">
              <h2>图片提示词案例</h2>
            </div>
            {promptCaseStatus === "loading" ? (
              <div className="wb-prompt-cases__grid wb-prompt-cases__grid--skeleton" aria-label="图片提示词案例加载中">
                {Array.from({ length: 8 }, (_, index) => (
                  <span key={index} className={`wb-prompt-case-skeleton wb-prompt-case-skeleton--${index % 4}`} />
                ))}
              </div>
            ) : promptCaseStatus === "error" ? (
              <div className="wb-prompt-cases__state">
                <strong>案例暂时没有加载成功</strong>
                <span>你仍然可以直接输入提示词开始生成。</span>
              </div>
            ) : promptCaseDisplayItems.length === 0 ? (
              <div className="wb-prompt-cases__state">
                <strong>暂无可展示案例</strong>
                <span>稍后会在这里展示社区精选提示词。</span>
              </div>
            ) : (
              <div className="wb-prompt-cases__grid">
                {promptCaseDisplayItems.map((item, index) => {
                  const measuredRatio = promptCaseMeasuredRatios[item.id];
                  return (
                    <button
                      key={item.id}
                      type="button"
                      className={getPromptCaseCardClassName(item, index, measuredRatio)}
                      onClick={() => setSelectedPromptCase(item)}
                    >
                      <img
                        src={item.imageUrl}
                        alt={item.title}
                        loading="lazy"
                        onLoad={(event) => handlePromptCaseImageLoad(item.id, event)}
                      />
                      <div>
                        <strong>{item.title}</strong>
                        <em>{item.author}</em>
                      </div>
                    </button>
                  );
                })}
              </div>
            )}
          </section>

        </div>
          </div>
          {renderConversationSidebar()}
        </div>
        {renderMessagePreviewOverlay()}
        {renderPromptCaseOverlay()}
        {renderDeleteDialog()}
        <OnboardingTour
          active={Boolean(effectiveOnboarding)}
          phase={tourPhase}
          stepIndex={tourStep}
          onNext={handleTourNext}
          onSkip={handleTourSkip}
          onDone={handleTourDone}
        />
      </section>
    );
  }

  // ─── ACTIVE STATE (conversation) ───
  return (
    <section
      className={`ai-workbench-page is-active mode-${activeMode} page-motion`}
      style={themeVars}
    >
      <input ref={referenceInputRef} type="file" accept={getReferenceAccept(activeMode, videoFrameMode)} multiple hidden onChange={handleReferenceUpload} />

      <div className="ai-workbench-shell">
        <div className="ai-workbench-main">
          <main className="ai-workbench-content-scroll">
            <section className="ai-workbench-thread-shell">
                <div className="ai-chat-main-panel">
                  <div className="ai-chat-messages-surface" ref={messagesSurfaceRef}>
                    <div className="ai-chat-message-list">
                      {projectError && (
                        <div className="conversation-sidebar__empty">
                          <span>Server data failed to load: {projectError}</span>
                        </div>
                      )}
                      {!projectError && messages.length === 0 && (
                        <div className="conversation-sidebar__empty">
                          <span>暂无记录内容</span>
                        </div>
                      )}
                      {messages.map((message) => (
                        <article key={message.id} className={`ai-chat-message-row chat-message-enter${message.role === "user" ? " is-user" : ""}`}>
                          <div className={`ai-chat-avatar${message.role === "user" ? " ai-chat-avatar--user" : ""}`}>
                            {message.role === "user" ? "我" : "AI"}
                          </div>
                          <div className="ai-chat-message-stack">
                            <div className="ai-chat-message-author">
                              <span>{message.author}</span>
                              <span>{message.createdAt}</span>
                            </div>
                            <div className={`ai-chat-message-bubble${message.role === "user" ? " ai-chat-message-bubble--user" : " ai-chat-message-bubble--assistant"}${message.status === "thinking" ? " is-thinking" : ""}`}>
                              {message.role === "user" && message.attachments && message.attachments.length > 0 && (
                                <div className="ai-chat-attachment-row">
                                  {message.attachments.map((item) => (
                                    <ChatAttachmentPreview key={`${message.id}-${item.token}`} item={item} onOpen={setMessagePreviewAttachment} />
                                  ))}
                                </div>
                              )}
                              {!(message.role === "assistant" && message.resultUrl && (message.mode === "image" || message.mode === "video")) && (
                                message.role === "user" ? (
                                  <p className="ai-chat-message-prompt">{message.body}</p>
                                ) : (
                                  <MarkdownMessage text={message.body} />
                                )
                              )}
                              {message.role !== "user" && message.attachments && message.attachments.length > 0 && (
                                <div className="ai-chat-attachment-row">
                                  {message.attachments.map((item) => (
                                    <ChatAttachmentPreview key={`${message.id}-${item.token}`} item={item} onOpen={setMessagePreviewAttachment} />
                                  ))}
                                </div>
                              )}
                              {(message.status === "failed" || message.status === "local_timeout") && message.role === "assistant" && (message.mode === "image" || message.mode === "video") && (
                                <div className="ai-chat-failed-actions">
                                  <button type="button" className="ai-chat-failed-actions__retry" onClick={() => handleRegenerate(message)}>
                                    <ReloadOutlined /> 重试
                                  </button>
                                  <button type="button" className="ai-chat-failed-actions__switch" onClick={() => { setToolbarMenuId(message.mode === "video" ? "video-model" : "image-model"); scrollMessagesSurface("bottom"); }}>
                                    <AppstoreOutlined /> 切换模型
                                  </button>
                                  <button type="button" className="ai-chat-failed-actions__release" onClick={() => handleReleaseStuckTask(message)}>
                                    <StopOutlined /> 释放卡住任务
                                  </button>
                                </div>
                              )}
                              {(message.status === "thinking" || message.status === "stopping") && !message.resultUrl && (message.mode === "image" || message.mode === "video") && (
                                <GenerationPendingCard message={message} onStop={() => handleStopSingleTask(message.id)} />
                              )}
                              {message.status === "thinking" && message.mode === "chat" && (
                                <div className="ai-chat-progress">
                                  <span>{message.taskStatusLabel || generationStatus}</span>
                                </div>
                              )}
                              {(message.resultUrl || (message.result && message.status !== "thinking")) && (
                                <ResultCard
                                  message={message}
                                  onRegenerate={handleRegenerate}
                                  onOpenMedia={setMessagePreviewAttachment}
                                  onSuperResolveVideo={handleSuperResolveVideo}
                                  onSuperResolveImage={handleSuperResolveImage}
                                  onOpenResultInCanvas={onOpenResultInCanvas}
                                  downloadFilenameBase={downloadFilenameBase}
                                />
                              )}
                            </div>
                          </div>
                        </article>
                      ))}
                      <div ref={messagesEndRef} />
                    </div>
                  </div>
                </div>
              </section>

            <section
              className={`wb-composer${composerHidden ? " is-hidden" : ""}${isComposerDragging ? " wb-composer--drag-active" : ""}`}
              ref={toolbarRef}
              onDragEnter={handleComposerDragEnter}
              onDragLeave={handleComposerDragLeave}
              onDragOver={handleComposerDragOver}
              onDrop={handleComposerDrop}
            >
              <div className="wb-composer__content">
                <div className="wb-composer__input-row">
                  {renderComposerReferences(false)}
                  <div className="wb-composer__main">
                    <textarea
                      ref={textareaRef}
                      className={`wb-composer__textarea${showPromptPreview ? " wb-composer__textarea--overlay-mode" : ""}`}
                      placeholder={composerPlaceholder}
                      value={inputValue}
                      disabled={false}
                      data-onboarding={obTarget({ chat: "onboarding-chat-input", image: "onboarding-image-input" })}
                      onChange={handlePromptChange}
                      onSelect={handlePromptSelectionChange}
                      onKeyUp={handlePromptSelectionChange}
                      onClick={handlePromptSelectionChange}
                      onMouseUp={handlePromptSelectionChange}
                      onKeyDown={handlePromptKeyDown}
                      onScroll={handlePromptScroll}
                    />
                    {showPromptPreview ? (
                      <PromptPreviewLayer text={inputValue} items={promptMentionOptions} onTokenPointerDown={focusPromptAt} />
                    ) : null}
                    {renderMentionPanel()}
                  </div>
                </div>
                {renderComposerToolbar(false, isGenerating)}
              </div>
            </section>
            <div className={`wb-chat-scroll-actions${scrollActionHint ? ` is-showing-${scrollActionHint}` : ""}`} aria-label="聊天滚动">
              <button
                type="button"
                className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--top"
                title="返回聊天顶部"
                aria-label="返回聊天顶部"
                onClick={() => scrollMessagesSurface("top")}
              >
                <ArrowUpOutlined />
              </button>
              <button
                type="button"
                className="wb-chat-scroll-actions__button wb-chat-scroll-actions__button--bottom"
                title="到达聊天底部"
                aria-label="到达聊天底部"
                onClick={() => scrollMessagesSurface("bottom")}
              >
                <ArrowDownOutlined />
              </button>
            </div>
          </main>
        </div>
        {renderConversationSidebar()}
      </div>

      {renderMessagePreviewOverlay()}
      {renderDeleteDialog()}
      {showRechargeModal && RechargeModal ? (
        <RechargeModal open={showRechargeModal} onClose={() => setShowRechargeModal(false)} currentBalance={session?.user?.balanceCents} />
      ) : null}

      <OnboardingTour
        active={Boolean(effectiveOnboarding)}
        phase={tourPhase}
        stepIndex={tourStep}
        onNext={handleTourNext}
        onSkip={handleTourSkip}
        onDone={handleTourDone}
      />
    </section>
  );
}

export default WorkbenchPage;
