import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { Dropdown, Message, Tooltip } from "@arco-design/web-react";
import { useTranslation } from "react-i18next";

import {
    calculateTaskPrice,
    createGenTask,
    getAiCategoryList,
    getAiModelList,
    getAiModelPrice,
    getAssetDetail,
    getParamConfig,
    getUserOperationDetail,
    recordDepointEvent,
    saveUserOperation,
} from "@/api/api";
import { MY_CREATIONS_REFRESH_QUERY, ROUTES } from "@/constants";
import { useComposerStore } from "@/store/useComposerStore";
import {
    useActiveTaskSubTypeStore,
    useCharacterToReferenceStore,
    useGlobalPromptImageStore,
    useMakeSameStore,
    useTemplateSelectStore,
    type GlobalPromptMediaIntent,
} from "@/store/useMakeSameStore";

import "./globalPromptPanel.scss";
import { ensureSufficientPoints, createComposerNavigate, ratioChipPreviewSize, uploadComposerMedia, uploadComposerMediaFiles } from "./composerCommon";
import { ActionMimicUploadPromptSection, type ActionMimicSelectedItem } from "./composerSections/ActionMimicUploadPromptSection";
import { CommonUploadPromptSection, type ComposerMediaItem, type ComposerMediaType } from "./composerSections/CommonUploadPromptSection";
import { HeadTailUploadPromptSection } from "./composerSections/HeadTailUploadPromptSection";
import { ReferenceUploadPromptSection } from "./composerSections/ReferenceUploadPromptSection";
import {
    GlobalTemplateComposerShell,
    type GlobalTemplateComposerSnapshot,
    type GlobalTemplateComposerShellHandle,
} from "./GlobalTemplateComposerShell/GlobalTemplateComposerShell";
import {
    TEMPLATE_COMPOSER_DEFAULT_PARAMS,
    buildTemplateComposerSnapshotFromBucket,
    buildTemplateComposerSnapshotFromTaskInfo,
} from "./GlobalTemplateComposerShell/templateComposerPreset";
import type { AiModelItem, AspectRatioItem, ComposerParamField, GlobalPromptDraftBucket, GlobalPromptDraftStoreV2, ReferenceSubjectItem } from "./types";
import { useHorizontalWheelScroll } from "./useHorizontalWheelScroll";
import {
    OTHER_CATEGORY,
    buildPreferredParamValues,
    buildComposerTaskParamPayload,
    buildSeedanceVideoPricePayload,
    buildSelectedParamsFromTaskInfo,
    buildGenTaskModelPayload,
    collectTaskInfoMedia,
    collectRejectedComposerMediaTypes,
    collectRejectedMediaTypesFromFiles,
    countComposerImages,
    dimensionPayloadFromPreview,
    getModelUploadImageLimit,
    getComposerParamFieldsForModel,
    getResolutionSizePreview,
    getTaskSubTypeRow,
    inferMediaTypeFromFile,
    inferMediaTypeFromUrl,
    isComposerMediaItemAllowedByModelMimeList,
    isFileAllowedByModelMimeList,
    isOptionMatch,
    isRatioField,
    isRestorableMediaUrl,
    parseDisplayDimensions,
    parseDraftStore,
    pickModelFromTaskInfo,
    readMediaDurationMap,
    splitUploadMediaItems,
} from "./utils";
import { closeAssetDetailsModal } from "@/dialog";
import { useUserStore } from "@/store/useUserStore";

/**
 * globalPromptPanel — 底部全局 AI 创作输入区（原 Layout 内联区块）
 *
 * ## 目录职责
 * | 文件 | 职责 |
 * |------|------|
 * | globalPromptPanel.tsx | 主壳：Tab、模型/参数、上传编排、草稿、跨页 intent、提交 |
 * | utils.ts | 纯函数：subType 映射、媒体解析、草稿 JSON、模型参数计算 |
 * | composerCommon.ts | 上传接口、积分校验 |
 * | types.ts | 本模块 TS 类型 |
 * | composerSections/* | 各 subType 专属输入 UI |
 * | GlobalTemplateComposerShell/ | subType 999 模板创作 |
 *
 * ## 任务 subType（`activeTaskSubType`）
 * - 102 文生图 · 103 图生图 · 202 生视频 · 203 参考生视频
 * - 204 首尾帧 · 205 动作模仿 · 999 模板（走 GlobalTemplateComposerShell）
 *
 * ## 主流程
 * 1. 挂载：分类列表 → 默认 subType → 拉模型 → 回填用户草稿（getUserOperationDetail）
 * 2. 编辑：`syncCurrentDraftBucketToStore` → 防抖 `saveUserOperation`
 * 3. 跨页：MakeSame / applyMedia / templateSelect / characterToReference 经 store intent 注入
 * 4. 提交：校验 → 积分 → createGenTask → 跳转「我的创作」或派发刷新事件
 *
 * ## 展示模式
 * - `floating`：Layout 底部悬浮（默认）
 * - `embedded`：嵌入 Create 等页，由 `embeddedVisible` 控制显隐
 */

const GLOBAL_PROMPT_SAVE_DEBOUNCE_MS = 600;
/** 提示词埋点 `recordDepointEvent` 防抖间隔 */
const PROMPT_DEPOINT_RECORD_DEBOUNCE_MS = 1000;

/** 创作区静态图标，避免每次 render 重复 `new URL` */
const COMPOSER_ASSET = {
    icHelp: new URL("@/assets/images/ic_help.png", import.meta.url).href,
    icArrowRightGray: new URL("/src/assets/images/ic_arrow_right_gray.png", import.meta.url).href,
    icLink: new URL("/src/assets/images/ic_link.png", import.meta.url).href,
    icModel: new URL("/src/assets/images/ic_model.png", import.meta.url).href,
    icRefresh: new URL("/src/assets/images/ic_refresh.png", import.meta.url).href,
    icStarsWhite: new URL("/src/assets/images/ic_stars_white.png", import.meta.url).href,
} as const;

function getDropdownPopupContainer() {
    return document.body;
}

/** 接口返回的 AI 产品分类 Tab 项 */
type AiCategoryItem = {
    taskSubType: number;
    name?: string;
    icon?: string;
    id?: number | string;
};

/**
 * 统一计算「生成」按钮上的消耗积分。
 * 分支：动作模仿固定公式 · billingMethod=2 走估价接口 · 其余走 calculateTaskPrice。
 */
function useGenerateCost(params: {
    activeModelItem: AiModelItem | null;
    selectedParamValues: Record<string, string | number>;
    activeTaskSubType: number | null;
    inputVideoDurationSeconds: number;
    isActionMimicTask: boolean;
    selectedActionMimicItem: ActionMimicSelectedItem | null;
}) {
    const { activeModelItem, selectedParamValues, activeTaskSubType, inputVideoDurationSeconds, isActionMimicTask, selectedActionMimicItem } = params;
    const [generateCost, setGenerateCost] = useState<number>(0);
    const debounceTimerRef = useRef<number | null>(null);
    const requestSeqRef = useRef(0);

    useEffect(() => {
        let cancelled = false;
        const computeGenerateCost = async () => {

            // 动作模仿 40 * 秒数 * 数量
            if (isActionMimicTask) {
                const duration = Number(selectedActionMimicItem?.duration ?? 0);
                const batchSize = Number(selectedParamValues.batchSize ?? 1);
                if (!Number.isFinite(duration) || duration <= 0) {
                    if (!cancelled) setGenerateCost(0);
                    return;
                }
                if (!cancelled) setGenerateCost(Math.ceil(duration * 40 * batchSize));
                return;
            }

            const modelParamFields = getComposerParamFieldsForModel(activeModelItem);
            const resolvedParamValues = buildComposerTaskParamPayload(modelParamFields, selectedParamValues);

            // token计费 从服务端拿积分
            if (activeModelItem?.billingMethod === 2) {
                const seedanceParams = buildSeedanceVideoPricePayload(modelParamFields, selectedParamValues);
                if (!seedanceParams) {
                    if (!cancelled) setGenerateCost(0);
                    return;
                }
                const currentSeq = ++requestSeqRef.current;
                const res: any = await getAiModelPrice({
                    modelName: activeModelItem?.aiModelCodeAlias,
                    estimationType: "seedance_video",
                    seedanceVideo: {
                        input_video_duration_seconds: inputVideoDurationSeconds,
                        ...seedanceParams,
                    },
                });
                const points = res?.data?.estimated_points ?? 0;
                const batchSize = Number(resolvedParamValues.batchSize ?? 1);
                if (!cancelled && currentSeq === requestSeqRef.current) {
                    setGenerateCost(Math.ceil(points * batchSize));
                }
                return;
            }

            // 其他任务 走后端价格计算接口。
            if (!activeModelItem?.code) {
                if (!cancelled) setGenerateCost(0);
                return;
            }
            const currentSeq = ++requestSeqRef.current;
            const res: any = await calculateTaskPrice({
                subType: activeTaskSubType,
                aiModelName: activeModelItem?.name,
                aiPlatform: activeModelItem?.code,
                aiModelId: activeModelItem?.id,
                model: activeModelItem?.code,
                aiModelCode: activeModelItem?.code,
                aiModelCodeAlias: activeModelItem?.aiModelCodeAlias,
                type: getTaskSubTypeRow(activeTaskSubType)?.type,
                ...resolvedParamValues,
            });
            const taskPrice = Number(res?.data?.taskPrice ?? 0);
            if (!cancelled && currentSeq === requestSeqRef.current) {
                setGenerateCost(Number.isFinite(taskPrice) && taskPrice > 0 ? Math.ceil(taskPrice) : 0);
            }
        };

        if (debounceTimerRef.current != null) {
            window.clearTimeout(debounceTimerRef.current);
            debounceTimerRef.current = null;
        }
        debounceTimerRef.current = window.setTimeout(() => {
            void computeGenerateCost().catch(() => {
                if (!cancelled) setGenerateCost(0);
            });
        }, 250);

        return () => {
            cancelled = true;
            if (debounceTimerRef.current != null) {
                window.clearTimeout(debounceTimerRef.current);
                debounceTimerRef.current = null;
            }
        };
    }, [activeModelItem, activeTaskSubType, inputVideoDurationSeconds, isActionMimicTask, selectedActionMimicItem, selectedParamValues]);

    return generateCost;
}

/**
 * 全局创作面板组件。
 *
 * @param mode `floating` 悬浮于 Layout 底部；`embedded` 嵌入子页面
 * @param embeddedVisible embedded 模式下是否展开
 * @param pathname / search / navigateTo 可选注入，便于 embedded 场景不依赖 window.location
 */
export type GlobalPromptPanelMode = "floating" | "embedded";

type GlobalPromptPanelProps = {
    mode?: GlobalPromptPanelMode;
    embeddedVisible?: boolean;
    pathname?: string;
    search?: string;
    navigateTo?: (to: { pathname: string; search?: string }, options?: { replace?: boolean }) => void;
};

export function GlobalPromptPanel({
    mode = "floating",
    embeddedVisible = false,
    pathname,
    search,
    navigateTo,
}: GlobalPromptPanelProps) {

    const setUser = useUserStore((s) => s.setUser);

    // ==================== 组件基础上下文 ====================
    const { t } = useTranslation();
    /** 路由信息：优先使用外部注入，缺省回退到 window.location。 */
    const currentPathname = pathname ?? globalThis?.location?.pathname ?? "";
    const currentSearch = search ?? globalThis?.location?.search ?? "";
    const navigateToMyCreations = useMemo(() => createComposerNavigate(navigateTo), [navigateTo]);

    // ==================== 本地状态：面板、模型、输入 ====================
    const [aiCategories, setAiCategories] = useState<AiCategoryItem[]>([]);
    const [activeTaskSubType, setActiveTaskSubType] = useState<number | null>(null); // 当前产品分类 ID
    const [aiModels, setAiModels] = useState<AiModelItem[]>([]); // AI 模型列表
    const [activeModelItem, setActiveModelItem] = useState<AiModelItem | null>(null); // 当前选中模型
    const [isComposerCollapsed, setIsComposerCollapsed] = useState(false); // 底部面板折叠状态
    const composerTabsWheelRef = useHorizontalWheelScroll<HTMLDivElement>({ enabled: !isComposerCollapsed });
    const composerBodyWheelRef = useHorizontalWheelScroll<HTMLDivElement>({
        enabled: true,
        ignoreWithin: "textarea, .arco-textarea, .referenceUploadPromptSection__promptEditor, .referenceUploadPromptSection__promptEditorWrap",
    });
    const [modelMenuOpen, setModelMenuOpen] = useState(false); // 模型下拉开关
    const [paramMenuOpen, setParamMenuOpen] = useState(false); // 参数下拉开关
    const [selectedParamValues, setSelectedParamValues] = useState<Record<string, string | number>>({}); // 已选参数值
    const [chatPrompt, setChatPrompt] = useState(""); // 输入框内容
    const activeTaskSubTypeRef = useRef<number | null>(activeTaskSubType);
    activeTaskSubTypeRef.current = activeTaskSubType;
    const recordPromptDepointTimerRef = useRef<number | null>(null);

    const handleChatPromptChange = useCallback((next: string) => {
        setChatPrompt(next);
        if (recordPromptDepointTimerRef.current != null) {
            window.clearTimeout(recordPromptDepointTimerRef.current);
            recordPromptDepointTimerRef.current = null;
        }
        recordPromptDepointTimerRef.current = window.setTimeout(() => {
            recordPromptDepointTimerRef.current = null;
            const subtype = activeTaskSubTypeRef.current ?? 0;
            void recordDepointEvent({
                eventType: "user_operation_prompt",
                targetId: '0',
                extra: {
                    subtype,
                    chatPrompt: next,
                },
            }).catch(() => {
                /* 埋点失败不影响输入 */
            });
        }, 1000);
    }, []);

    useEffect(() => {
        return () => {
            if (recordPromptDepointTimerRef.current != null) {
                window.clearTimeout(recordPromptDepointTimerRef.current);
                recordPromptDepointTimerRef.current = null;
            }
        };
    }, []);
    const supportedUploadMimePrefixes = useMemo(() => {
        const prefixes: string[] = [];
        if (activeModelItem?.isSupportImages ?? true) prefixes.push("image/");
        if (activeModelItem?.isSupportVideos) prefixes.push("video/");
        if (activeModelItem?.isSupportAudios) prefixes.push("audio/");
        return prefixes;
    }, [activeModelItem?.isSupportAudios, activeModelItem?.isSupportImages, activeModelItem?.isSupportVideos]);
    /** 系统文件框对仅有 `audio/*` 时可能不展示 .mp3，需并列写出扩展名（含大写 .MP3）。 */
    const uploadAccept = useMemo(() => {
        const parts: string[] = [];
        if (activeModelItem?.isSupportImages ?? true) {
            parts.push("image/*", ".jpg", ".jpeg", ".png", ".webp", ".gif", ".bmp", ".svg");
        }
        if (activeModelItem?.isSupportVideos) {
            parts.push("video/*", ".mp4", ".mov", ".webm", ".m4v", ".avi", ".mkv");
        }
        if (activeModelItem?.isSupportAudios) {
            parts.push("audio/*", ".mp3", ".MP3", ".m4a", ".wav", ".aac", ".ogg", ".flac", ".opus");
        }
        return parts.join(",");
    }, [activeModelItem?.isSupportAudios, activeModelItem?.isSupportImages, activeModelItem?.isSupportVideos]);

    const [usageTutorialPageUrl, setUsageTutorialPageUrl] = useState("");

    useEffect(() => {
        void getParamConfig()
            .then((res) => {
                setUsageTutorialPageUrl(res.systemConfig.usageTutorialPageUrl ?? "");
            })
            .catch(() => {
                setUsageTutorialPageUrl("");
            });
    }, []);

    // ==================== 全局 store：跨页面消息与控制 ====================
    const makeSameIntent = useMakeSameStore((s) => s.intent); // 制作同款意图
    const makeSameTemplateIntentSeq = useMakeSameStore((s) => (s.intent?.mode === "template" ? s.intent.seq : 0));

    /** 仅 seq 变化时消费一次，避免 intent effect 因 composer 状态变化反复触发 */
    const applyMediaIntentSeq = useGlobalPromptImageStore((s) => s.intent?.seq ?? 0);

    const activeTaskSubTypeIntent = useActiveTaskSubTypeStore((s) => s.intent); // 当前产品分类意图

    const applyCharacterIntent = useCharacterToReferenceStore((s) => s.intent); // 应用角色到参考意图

    const templateSelectIntentSeq = useTemplateSelectStore((s) => s.intent?.seq ?? 0);

    // ==================== 引用与流程状态：上传、并发控制、草稿缓存 ====================
    const [mediaItems, setMediaItems] = useState<ComposerMediaItem[]>([]); // 已上传媒体列表
    const [selectedActionMimicItem, setSelectedActionMimicItem] = useState<ActionMimicSelectedItem | null>(null);
    const [templateVideoMaterialId, setTemplateVideoMaterialId] = useState<number | string | null>(null);
    const [referenceStructure, setReferenceStructure] = useState<{
        referenceSubjectList: ReferenceSubjectItem[];
    }>({
        referenceSubjectList: [],
    });
    const pendingReferenceCharacterRef = useRef<{ id: number | string; title: string; threeViewImage: string } | null>(null);
    const applyCharacterToReferenceHandlerRef = useRef<((payload: { id: number | string; title: string; threeViewImage: string }) => void) | null>(null);
    const pendingHeadTailSlotRef = useRef<0 | 1 | null>(null); // 当前要替换的槽位（0:首帧,1:尾帧）
    /** 制作同款已手动拿过该 subType 模型时，按 subType 精确跳过一次 effect 拉取。 */
    const skipModelFetchForSubTypeRef = useRef<number | null>(null);
    /** 防止 applyMedia intent 在 StrictMode 下重复消费同一 seq */
    const applyMediaIntentProcessingSeqRef = useRef(0);
    /** 创建任务流程进行中，防止重复点击重复提交（ref 同步挡并发，state 驱动按钮 disabled） */
    const createGenTaskInFlightRef = useRef(false);
    const [isCreatingGenTask, setIsCreatingGenTask] = useState(false);
    const templateApplyMediaHandlerRef = useRef<
        ((urls: string[], mode: "append" | "replace") => void) | null
    >(null);
    /**
     * 制作同款完成回填后自动创建任务。
     */
    const pendingAutoCreateAfterMakeSameRef = useRef(false);
    /** subType -> 模型列表缓存，减少同参数重复请求。 */
    const aiModelCacheRef = useRef<Map<number, AiModelItem[]>>(new Map());
    const templateComposerRef = useRef<GlobalTemplateComposerShellHandle | null>(null);
    const [templateDraftSnapshot, setTemplateDraftSnapshot] = useState<GlobalTemplateComposerSnapshot>({
        selectedTemplateId: null,
        images: [],
        selectedParamValues: { ...TEMPLATE_COMPOSER_DEFAULT_PARAMS },
    });
    const templateDraftRef = useRef<GlobalTemplateComposerSnapshot>(templateDraftSnapshot);
    /** 与 state 同步，供 Tab 切换与 syncStore 读取，避免时序导致读到旧快照。 */
    templateDraftRef.current = templateDraftSnapshot;
    const composerChromeIntent = useComposerStore((s) => s.intent);
    const requestComposerCollapse = useComposerStore((s) => s.requestComposerCollapse);
    const hasLoadedUserOperationRef = useRef(false);
    /** 如果初始化阶段已处理外部意图，忽略晚到的草稿回填覆盖。 */
    const hasExternalInitIntentRef = useRef(false);
    const draftStoreRef = useRef<GlobalPromptDraftStoreV2>({
        version: 2,
        currentProductType: null,
        draftsBySubType: {},
    });
    const latestSavedDraftStoreRef = useRef("");
    /** 防抖保存 timer 触发时读取最新合并结果，避免 600ms 窗口内状态已变却写入旧快照 */
    const syncCurrentDraftBucketToStoreRef = useRef<(override?: {
        subType?: number | null;
        templateSnapshot?: GlobalTemplateComposerSnapshot;
    }) => GlobalPromptDraftStoreV2>(() => draftStoreRef.current);
    /** 从草稿桶切回某分类后，等 `aiModels` 就绪再恢复模型与下方参数预设。 */
    const pendingDraftComposerRestoreRef = useRef<{
        subType: number;
        modelId: string | number | null;
        paramValues: Record<string, string | number>;
    } | null>(null);

    const isHeadTailTask = activeTaskSubType === 204;//首尾帧
    const isReferenceTask = activeTaskSubType === 203;//参考生视频
    const isActionMimicTask = activeTaskSubType === 205;//动作模仿
    const isTemplateTask = activeTaskSubType === 999;//模板

    // ==================== 派生数据：模型上传能力 ====================
    /** 当前模型对图片数量的上限；null 表示不限制图片张数。 */
    const modelUploadImageLimit = useMemo(() => getModelUploadImageLimit(activeModelItem), [activeModelItem]);
    /** 模型是否支持上传视频/音频（此类媒体不做数量上限，仅图片受 uploadImageLimit 约束）。 */
    const allowNonImageUpload = Boolean(activeModelItem?.isSupportVideos || activeModelItem?.isSupportAudios);
    const composerMediaUploadEnabled = supportedUploadMimePrefixes.length > 0;
    const taskSubTypePlaceholder = useMemo(() => {
        const row = getTaskSubTypeRow(activeTaskSubType);
        if (!row || !("placeholder" in row)) return undefined;
        return t(row.placeholder);
    }, [activeTaskSubType, t]);

    const handleComposerChromeExpand = useCallback(() => {
        setIsComposerCollapsed(false);
    }, []);

    const handleTemplateApplyMediaHandlerRegister = useCallback(
        (handler: ((urls: string[], mode: "append" | "replace") => void) | null) => {
            templateApplyMediaHandlerRef.current = handler;
        },
        [],
    );

    const applyDraftBucketToComposer = useCallback((subType: number | null, store: GlobalPromptDraftStoreV2) => {
        if (subType == null) return;
        const bucket = store.draftsBySubType[String(subType)] ?? {};
        if (subType === 999) {
            pendingDraftComposerRestoreRef.current = null;
            const snapshot = buildTemplateComposerSnapshotFromBucket(bucket);
            setTemplateDraftSnapshot(snapshot);
            templateComposerRef.current?.setSnapshot(snapshot);
            return;
        }
        pendingDraftComposerRestoreRef.current = {
            subType,
            modelId: bucket.selectedAiModelId ?? null,
            paramValues:
                bucket.selectedParamValues && typeof bucket.selectedParamValues === "object" && !Array.isArray(bucket.selectedParamValues)
                    ? { ...bucket.selectedParamValues }
                    : {},
        };
        pendingHeadTailSlotRef.current = null;
        setChatPrompt(bucket.chatPrompt ?? "");
        setMediaItems(Array.isArray(bucket.mediaItems) ? bucket.mediaItems : []);
        setSelectedActionMimicItem(null);
        setTemplateVideoMaterialId(null);

        if (subType === 203) {
            setReferenceStructure({
                referenceSubjectList: Array.isArray(bucket.referenceSubjectList) ? bucket.referenceSubjectList : [],
            });
            return;
        }
        setReferenceStructure({ referenceSubjectList: [] });

        if (subType === 205) {
            const officialId = bucket.actionMimicTemplateVideoMaterialId;
            if (officialId != null && officialId !== "") {
                setTemplateVideoMaterialId(officialId);
            }
            return;
        }
    }, []);

    const syncCurrentDraftBucketToStore = useCallback(
        (override?: { subType?: number | null; templateSnapshot?: GlobalTemplateComposerSnapshot }) => {
            const subType = override?.subType ?? activeTaskSubType;
            if (subType == null) return draftStoreRef.current;
            /** 999 优先读子组件实时快照；onSnapshotChange 有 200ms 防抖，仅用 state 会在保存窗口内丢参考图/参数。 */
            const templateSnapshot =
                override?.templateSnapshot ??
                (subType === 999
                    ? (templateComposerRef.current?.getSnapshot() ?? templateDraftSnapshot)
                    : templateDraftRef.current);
            const key = String(subType);
            const composerModelParams: Pick<GlobalPromptDraftBucket, "selectedAiModelId" | "selectedParamValues"> = {
                selectedAiModelId: activeModelItem?.id ?? null,
                selectedParamValues: { ...selectedParamValues },
            };
            const bucket: GlobalPromptDraftBucket =
                subType === 999
                    ? {
                        selectedTemplateId: templateSnapshot.selectedTemplateId ?? null,
                        templateImages: (templateSnapshot.images ?? []).filter((url) => isRestorableMediaUrl(url)),
                        selectedParamValues: {
                            ...TEMPLATE_COMPOSER_DEFAULT_PARAMS,
                            ...(templateSnapshot.selectedParamValues &&
                                typeof templateSnapshot.selectedParamValues === "object" &&
                                !Array.isArray(templateSnapshot.selectedParamValues)
                                ? templateSnapshot.selectedParamValues
                                : {}),
                        },
                        updatedAt: Date.now(),
                    }
                    : subType === 205
                        ? {
                            chatPrompt,
                            mediaItems: mediaItems.filter((item) => isRestorableMediaUrl(item.url)),
                            actionMimicTemplateVideoMaterialId:
                                selectedActionMimicItem?.type === "官方示例" ? selectedActionMimicItem.id : null,
                            ...composerModelParams,
                            updatedAt: Date.now(),
                        }
                        : {
                            chatPrompt,
                            mediaItems: mediaItems.filter((item) => isRestorableMediaUrl(item.url)),
                            referenceSubjectList: subType === 203 ? referenceStructure.referenceSubjectList : [],
                            ...composerModelParams,
                            updatedAt: Date.now(),
                        };
            const nextStore: GlobalPromptDraftStoreV2 = {
                version: 2,
                currentProductType: subType,
                draftsBySubType: {
                    ...draftStoreRef.current.draftsBySubType,
                    [key]: {
                        ...draftStoreRef.current.draftsBySubType[key],
                        ...bucket,
                    },
                },
            };
            draftStoreRef.current = nextStore;
            return nextStore;
        },
        [
            activeTaskSubType,
            activeModelItem?.id,
            chatPrompt,
            mediaItems,
            referenceStructure.referenceSubjectList,
            selectedActionMimicItem,
            selectedParamValues,
            templateDraftSnapshot,
        ],
    );
    syncCurrentDraftBucketToStoreRef.current = syncCurrentDraftBucketToStore;

    // ==================== 交互处理：上传入口与图片管理 ====================
    /** 首尾帧：写入指定槽位并展开面板 */
    const commitHeadTailSlotItem = useCallback((item: ComposerMediaItem) => {
        const targetSlot = pendingHeadTailSlotRef.current ?? 0;
        setMediaItems((prev) => {
            const next = [...prev];
            next[targetSlot] = item;
            return next;
        });
        pendingHeadTailSlotRef.current = null;
        setIsComposerCollapsed(false);
    }, []);

    /** 当前模型不支持某类媒体时，按类型提示（每类最多一条）。 */
    const warnUnsupportedComposerMediaTypes = useCallback(
        (rejectedTypes: ComposerMediaType[]) => {
            for (const type of rejectedTypes) {
                if (type === "video") Message.warning(t("composer.unsupportedUploadVideo"));
                else if (type === "audio") Message.warning(t("composer.unsupportedUploadAudio"));
                else if (type === "image") Message.warning(t("composer.unsupportedUploadImage"));
            }
        },
        [t],
    );

    /** 按模型图片上限与 mime 能力筛选可接受的媒体项，并在超限时提示。 */
    const pickAcceptedComposerMediaItems = useCallback(
        (prev: ComposerMediaItem[], incoming: ComposerMediaItem[]) => {
            let imagesLeft =
                modelUploadImageLimit == null ? Number.POSITIVE_INFINITY : Math.max(0, modelUploadImageLimit - countComposerImages(prev));
            const accepted: ComposerMediaItem[] = [];
            let imageLimitWarned = false;
            const rejectedTypes = collectRejectedComposerMediaTypes(incoming, supportedUploadMimePrefixes);
            for (const item of incoming) {
                if (!isComposerMediaItemAllowedByModelMimeList(item, supportedUploadMimePrefixes)) {
                    continue;
                }
                if (item.type === "image" && imagesLeft <= 0) {
                    if (!imageLimitWarned && modelUploadImageLimit != null) {
                        Message.warning(t("composer.maxUploadReached", { max: modelUploadImageLimit }));
                        imageLimitWarned = true;
                    }
                    continue;
                }
                accepted.push(item);
                if (item.type === "image") imagesLeft -= 1;
            }
            if (rejectedTypes.length > 0) {
                warnUnsupportedComposerMediaTypes(rejectedTypes);
            }
            if (accepted.length < incoming.length && accepted.length > 0 && !imageLimitWarned && modelUploadImageLimit != null) {
                Message.warning(t("composer.maxUploadTrimmed", { max: modelUploadImageLimit, count: accepted.length }));
            }
            return accepted;
        },
        [modelUploadImageLimit, supportedUploadMimePrefixes, t, warnUnsupportedComposerMediaTypes],
    );

    /** 非首尾帧：根据待上传文件计算可接受数量；满额则提示并返回 null */
    const resolveBatchUploadCapacity = useCallback(
        (files: File[]) => {
            if (files.length === 0) return 0;
            const pseudoItems = files.map((file) => ({ type: inferMediaTypeFromFile(file), url: "" } as ComposerMediaItem));
            const accepted = pickAcceptedComposerMediaItems(mediaItems, pseudoItems);
            if (accepted.length === 0) {
                setIsComposerCollapsed(false);
                return null;
            }
            if (accepted.length < files.length && accepted.length > 0 && modelUploadImageLimit != null) {
                Message.warning(t("composer.maxUploadTrimmed", { max: modelUploadImageLimit, count: accepted.length }));
            }
            return accepted.length;
        },
        [mediaItems, modelUploadImageLimit, pickAcceptedComposerMediaItems, t],
    );

    const appendUploadedMediaItems = useCallback((uploadedItems: ComposerMediaItem[]) => {
        if (uploadedItems.length === 0) return;
        setMediaItems((prev) => {
            const accepted = pickAcceptedComposerMediaItems(prev, uploadedItems);
            if (accepted.length === 0) {
                setIsComposerCollapsed(false);
                return prev;
            }
            setIsComposerCollapsed(false);
            return [...prev, ...accepted];
        });
    }, [pickAcceptedComposerMediaItems]);

    /** 跨页 replace：整表替换为传入媒体（仍受总槽位与模型图片上限约束） */
    const replaceUploadedMediaItems = useCallback(
        (nextItems: ComposerMediaItem[]) => {
            const restorable = nextItems.filter((item) => isRestorableMediaUrl(item.url));
            const accepted = pickAcceptedComposerMediaItems([], restorable);
            if (accepted.length === 0) return;
            setMediaItems(accepted);
            setIsComposerCollapsed(false);
        },
        [pickAcceptedComposerMediaItems],
    );

    /** 统一处理本地文件上传（input / drop 复用） */
    const uploadMediaFiles = useCallback(async (files: File[]) => {
        try {
            const rejectedTypes = collectRejectedMediaTypesFromFiles(files, supportedUploadMimePrefixes);
            if (rejectedTypes.length > 0) {
                warnUnsupportedComposerMediaTypes(rejectedTypes);
            }
            const acceptedFiles = files.filter((file) => isFileAllowedByModelMimeList(file, supportedUploadMimePrefixes));
            if (acceptedFiles.length === 0) return;
            const durationMap = await readMediaDurationMap(acceptedFiles);

            if (isHeadTailTask) {
                const firstFile = acceptedFiles[0];
                // 首尾帧模式一次只处理一个文件，并写入当前选中的槽位。
                const first = await uploadComposerMedia(firstFile);
                const headTailType = inferMediaTypeFromFile(firstFile);
                commitHeadTailSlotItem({ type: headTailType, url: first.url, thumbUrl: first.thumbUrl, duration: durationMap.get(firstFile) });
                return;
            }
            const availableCount = resolveBatchUploadCapacity(acceptedFiles);
            if (availableCount == null) return;
            const uploadTargets = acceptedFiles.slice(0, availableCount);
            const uploaded = await uploadComposerMediaFiles(uploadTargets);
            const uploadedItems = uploaded.map((item, idx) => {
                const file = uploadTargets[idx]!;
                return { type: inferMediaTypeFromFile(file), url: item.url, thumbUrl: item.thumbUrl, duration: durationMap.get(file) } as ComposerMediaItem;
            });
            appendUploadedMediaItems(uploadedItems);
        } catch {
            Message.error(t("composer.uploadFailed"));
        }
    }, [appendUploadedMediaItems, commitHeadTailSlotItem, isHeadTailTask, resolveBatchUploadCapacity, supportedUploadMimePrefixes, t, warnUnsupportedComposerMediaTypes]);

    /** 处理拖拽进来的媒体 URL */
    const appendMediaUrls = useCallback((rawUrls: string[]) => {
        const parsedItems = Array.from(new Set(rawUrls.map((item) => item.trim()).filter(isRestorableMediaUrl))).map(
            (url) => ({ type: inferMediaTypeFromUrl(url), url } as ComposerMediaItem),
        );
        const rejectedTypes = collectRejectedComposerMediaTypes(parsedItems, supportedUploadMimePrefixes);
        if (rejectedTypes.length > 0) {
            warnUnsupportedComposerMediaTypes(rejectedTypes);
        }
        const mediaItemsFromUrls = parsedItems.filter((item) =>
            isComposerMediaItemAllowedByModelMimeList(item, supportedUploadMimePrefixes),
        );
        if (mediaItemsFromUrls.length === 0) return;
        if (isHeadTailTask) {
            const first = mediaItemsFromUrls[0];
            if (first) commitHeadTailSlotItem(first);
            return;
        }
        appendUploadedMediaItems(mediaItemsFromUrls);
    }, [appendUploadedMediaItems, commitHeadTailSlotItem, isHeadTailTask, supportedUploadMimePrefixes, warnUnsupportedComposerMediaTypes]);

    /** 选择本地图片：立即上传，成功后回显远端地址 */
    const handleUploadInputChange = useCallback((evt: React.ChangeEvent<HTMLInputElement>) => {
        const fileList = evt.target.files;
        if (!fileList || fileList.length === 0) return;
        void uploadMediaFiles(Array.from(fileList));
        evt.target.value = "";
    }, [uploadMediaFiles]);

    /** 拖拽悬停 / 离开 / 放下 */
    const handleUploadAreaDrop = useCallback((evt: React.DragEvent<HTMLDivElement>) => {
        evt.preventDefault();
        evt.stopPropagation();
        const fileList = evt.dataTransfer?.files;
        if (fileList && fileList.length > 0) {
            void uploadMediaFiles(Array.from(fileList));
            return;
        }

        const uriListRaw = evt.dataTransfer.getData("text/uri-list");
        const plainTextRaw = evt.dataTransfer.getData("text/plain");
        const uriList = uriListRaw
            .split("\n")
            .map((line) => line.trim())
            .filter((line) => line && !line.startsWith("#"));
        const plainList = plainTextRaw ? [plainTextRaw.trim()] : [];
        appendMediaUrls(Array.from(new Set([...uriList, ...plainList])));
    }, [appendMediaUrls, uploadMediaFiles]);

    /** 删除指定位置图片 */
    const handleRemoveMedia = useCallback((targetIdx: number) => {
        setMediaItems((prev) => prev.filter((_, idx) => idx !== targetIdx));
    }, []);

    /** 按 subjectId 恢复被剪切掉的图片，确保标签编号与图片顺序对齐。 */
    const handleRestoreSubjectImages = useCallback((items: Array<{ subjectId: number; type: "image" | "video" | "audio"; url: string; thumbUrl?: string; duration?: number }>) => {
        if (!Array.isArray(items) || items.length === 0) return;
        setMediaItems((prev) => {
            let next = [...prev];
            const sorted = [...items]
                .filter((item) => Number.isFinite(item.subjectId) && item.subjectId > 0 && Boolean(item.url))
                .sort((a, b) => a.subjectId - b.subjectId);
            sorted.forEach((item) => {
                let insertIdx = Math.min(Math.max(item.subjectId - 1, 0), next.length);
                const existedIdx = next.findIndex((mediaItem) => mediaItem.url === item.url && mediaItem.type === item.type);
                if (existedIdx === insertIdx) return;
                if (existedIdx >= 0) {
                    next.splice(existedIdx, 1);
                    if (existedIdx < insertIdx) insertIdx -= 1;
                }
                next.splice(insertIdx, 0, { type: item.type, url: item.url, thumbUrl: item.thumbUrl, duration: item.duration });
            });
            if (modelUploadImageLimit == null) return next;
            let imageCount = 0;
            return next.filter((item) => {
                if (item.type !== "image") return true;
                if (imageCount >= modelUploadImageLimit) return false;
                imageCount += 1;
                return true;
            });
        });
    }, [modelUploadImageLimit]);

    /**
     * 清空非模板模式的编辑态。
     * 这样可以让「重置」和「切换任务类型」共用同一套状态重置逻辑，避免分叉。
     */
    const resetNonTemplateComposerState = useCallback(() => {
        setMediaItems([]);
        setChatPrompt("");
        setSelectedActionMimicItem(null);
        setTemplateVideoMaterialId(null);
        setReferenceStructure({
            referenceSubjectList: [],
        });
        pendingHeadTailSlotRef.current = null;
    }, []);

    /** 重置输入与上传内容 */
    const handleResetComposer = useCallback(() => {
        if (isTemplateTask) {
            templateComposerRef.current?.reset();
            setTemplateDraftSnapshot({
                selectedTemplateId: null,
                images: [],
                selectedParamValues: { ...TEMPLATE_COMPOSER_DEFAULT_PARAMS },
            });
            return;
        }
        resetNonTemplateComposerState();
    }, [isTemplateTask, resetNonTemplateComposerState]);

    // ==================== 面板 UI 同步：显示/折叠状态 ====================
    useEffect(() => {
        if (!composerChromeIntent) return;
        setIsComposerCollapsed(composerChromeIntent.action === "collapse");
    }, [composerChromeIntent]);

    // ==================== 初始化数据：分类与模型 ====================
    useEffect(() => {
        void getAiCategoryList()
            .then((res: { data?: { list?: unknown[] } }) => {
                const rawList = res.data?.list ? [...res.data.list, ...OTHER_CATEGORY] : [];
                const list = rawList
                    .map((item): AiCategoryItem | null => {
                        if (!item || typeof item !== "object") return null;
                        const row = item as Record<string, unknown>;
                        const subTypeNum = Number(row.taskSubType);
                        if (!Number.isFinite(subTypeNum)) return null;
                        return {
                            ...row,
                            taskSubType: subTypeNum,
                            name: typeof row.name === "string" ? row.name : undefined,
                            icon: typeof row.icon === "string" ? row.icon : undefined,
                            id: typeof row.id === "number" || typeof row.id === "string" ? row.id : undefined,
                        };
                    })
                    .filter((item): item is AiCategoryItem => item != null && item.taskSubType !== 106);
                setAiCategories(list);
            })
            .catch(() => {
                setAiCategories([]);
            });
    }, []);

    /** 初始化当前产品分类 ID */
    useEffect(() => {
        if (aiCategories.length === 0) return;
        setActiveTaskSubType((prev) => {
            const prevNum = prev == null ? null : Number(prev);
            if (prevNum != null && Number.isFinite(prevNum) && aiCategories.some((row) => Number(row.taskSubType) === prevNum)) return prevNum;
            return aiCategories[0].taskSubType;
        });
    }, [aiCategories]);

    /** 按 subType 拉模型列表并缓存；`displayDimensions` 从 JSON 字符串解析为对象。 */
    const fetchAiModelsForSubType = useCallback(async (subType: number): Promise<AiModelItem[]> => {
        const cached = aiModelCacheRef.current.get(subType);
        if (cached) return cached;
        const res = (await getAiModelList({ subType })) as { data?: AiModelItem[] };
        const list = Array.isArray(res.data) ? res.data : [];
        list.forEach((item) => {
            item.displayDimensions = parseDisplayDimensions(item.displayDimensions);
        });
        aiModelCacheRef.current.set(subType, list);
        return list;
    }, []);

    const applyAiModelsList = useCallback((list: AiModelItem[]) => {
        setAiModels(list);
        const firstModel = list[0] ?? null;
        setActiveModelItem(firstModel);
        setSelectedParamValues(buildPreferredParamValues(getComposerParamFieldsForModel(firstModel), {}));
    }, []);

    /** 拉模型并写状态；制作同款可选择只取列表不落地，避免中间态覆盖。 */
    const loadModelsForSubType = useCallback(
        async (subType: number, options?: { applyToState?: boolean }) => {
            const shouldApplyToState = options?.applyToState ?? true;
            try {
                const list = await fetchAiModelsForSubType(subType);
                if (shouldApplyToState) {
                    applyAiModelsList(list);
                }
                return list;
            } catch {
                if (shouldApplyToState) {
                    applyAiModelsList([]);
                }
                return [];
            }
        },
        [applyAiModelsList, fetchAiModelsForSubType],
    );

    /**
     * 跨页 applyMedia intent 统一入口（对齐制作同款：先占住 subType 拉模，再写媒体/提示词，保证积分重算）。
     * 仅由 store seq 触发一次，不订阅易变的 composer 状态。
     */
    const applyExternalMediaIntent = useCallback(
        async (intent: GlobalPromptMediaIntent) => {
            hasExternalInitIntentRef.current = true;

            const targetSubType = intent.subType ?? activeTaskSubTypeRef.current;
            const normalizedItems = intent.mediaItems
                .map((item) => ({
                    type: item.type ?? inferMediaTypeFromUrl(item.url),
                    url: item.url.trim(),
                    thumbUrl: item.thumbUrl,
                    duration: item.duration,
                }))
                .filter((item) => isRestorableMediaUrl(item.url)) as ComposerMediaItem[];

            const subTypeChanges =
                targetSubType != null && targetSubType !== activeTaskSubTypeRef.current;
            const aiModelCode = intent.aiModelCode?.trim() ?? "";
            const willLoadModelByCode = Boolean(aiModelCode) && targetSubType != null && targetSubType !== 999;

            if (subTypeChanges && targetSubType != null) {
                const latestStore = syncCurrentDraftBucketToStore();
                applyDraftBucketToComposer(targetSubType, latestStore);
                if (willLoadModelByCode) {
                    skipModelFetchForSubTypeRef.current = targetSubType;
                }
                setActiveTaskSubType(targetSubType);
            }

            if (intent.prompt !== undefined) {
                setChatPrompt(intent.prompt);
            }

            if (normalizedItems.length > 0 && targetSubType != null) {
                const mediaMode = intent.mediaMode;
                if (targetSubType === 999) {
                    templateApplyMediaHandlerRef.current?.(
                        normalizedItems.map((item) => item.url),
                        mediaMode,
                    );
                } else if (targetSubType === 204) {
                    if (mediaMode === "replace") {
                        replaceUploadedMediaItems(normalizedItems.slice(0, 2));
                    } else if (normalizedItems.length === 1) {
                        commitHeadTailSlotItem(normalizedItems[0]!);
                    } else {
                        setMediaItems(normalizedItems.slice(0, 2));
                        setIsComposerCollapsed(false);
                    }
                } else if (mediaMode === "replace") {
                    replaceUploadedMediaItems(normalizedItems);
                } else {
                    appendUploadedMediaItems(normalizedItems);
                }
            }

            if (willLoadModelByCode && targetSubType != null) {
                skipModelFetchForSubTypeRef.current = targetSubType;
                try {
                    const list = await loadModelsForSubType(targetSubType, { applyToState: false });
                    const model = pickModelFromTaskInfo(list, { aiModelCode });
                    setAiModels(list);
                    setActiveModelItem(model);
                    const fields = getComposerParamFieldsForModel(model);
                    setSelectedParamValues(buildPreferredParamValues(fields, {}));
                } catch {
                    applyAiModelsList([]);
                }
            }

            setIsComposerCollapsed(false);
        },
        [
            appendUploadedMediaItems,
            applyAiModelsList,
            applyDraftBucketToComposer,
            commitHeadTailSlotItem,
            loadModelsForSubType,
            replaceUploadedMediaItems,
            syncCurrentDraftBucketToStore,
        ],
    );

    /** 写入模板 snapshot 并切到 999（子组件未挂载时靠 initialSnapshot 承接）。 */
    const activateTemplateComposer = useCallback((snapshot: GlobalTemplateComposerSnapshot) => {
        setTemplateDraftSnapshot(snapshot);
        templateComposerRef.current?.setSnapshot(snapshot);
        setActiveTaskSubType(999);
        setIsComposerCollapsed(false);
    }, []);

    /** subType 变化时拉模型；制作同款会先标记 subType，避免与本 effect 重复请求。 */
    useEffect(() => {
        if (activeTaskSubType == null) return;
        if (skipModelFetchForSubTypeRef.current === activeTaskSubType) {
            skipModelFetchForSubTypeRef.current = null;
            return;
        }
        let cancelled = false;
        void (async () => {
            try {
                const list = await fetchAiModelsForSubType(activeTaskSubType);
                if (cancelled) return;
                const pending = pendingDraftComposerRestoreRef.current;
                if (pending && pending.subType === activeTaskSubType && list.length > 0) {
                    pendingDraftComposerRestoreRef.current = null;
                    const model = pickModelFromTaskInfo(list, { aiModelId: pending.modelId } as Record<string, unknown>);
                    setAiModels(list);
                    setActiveModelItem(model);
                    const fields = getComposerParamFieldsForModel(model);
                    setSelectedParamValues(buildPreferredParamValues(fields, pending.paramValues));
                } else {
                    applyAiModelsList(list);
                }
            } catch {
                if (!cancelled) applyAiModelsList([]);
            }
        })();
        return () => {
            cancelled = true;
        };
    }, [activeTaskSubType, applyAiModelsList, fetchAiModelsForSubType]);

    // ==================== 跨页联动：制作同款、模板选择、外部追加图片 ====================
    /** 探索页「制作同款」等入口通过 store 传入作品 id，在此统一处理 */
    const applyMakeSameFromExplore = useCallback(
        async (postId: number | string, options?: { autoCreateAfterFill?: boolean }) => {
            hasExternalInitIntentRef.current = true;
            pendingDraftComposerRestoreRef.current = null;
            syncCurrentDraftBucketToStore();
            const res: any = await getAssetDetail(postId);
            const asset = res?.data;
            if (!asset) return;
            const taskInfo = asset.taskInfo as Record<string, unknown> | undefined;
            const mergedMedia = collectTaskInfoMedia(taskInfo);
            setChatPrompt(typeof taskInfo?.chatPrompt === "string" ? taskInfo.chatPrompt : "");
            const subType = Number(taskInfo?.subType ?? 102);
            const safeSubType = Number.isFinite(subType) ? subType : 102;
            if (safeSubType === 205) {
                setMediaItems(mergedMedia.filter((item) => item.type !== "video"));
                setSelectedActionMimicItem(null);
                setTemplateVideoMaterialId(
                    typeof taskInfo?.templateVideoMaterialId === "number" || typeof taskInfo?.templateVideoMaterialId === "string"
                        ? taskInfo.templateVideoMaterialId
                        : null,
                );
            } else {
                setMediaItems(mergedMedia);
                setTemplateVideoMaterialId(null);
            }

            skipModelFetchForSubTypeRef.current = safeSubType;
            setActiveTaskSubType(safeSubType);
            const list = await loadModelsForSubType(safeSubType, { applyToState: false });
            const model = pickModelFromTaskInfo(list, taskInfo);
            setAiModels(list);
            setActiveModelItem(model);
            const fields = getComposerParamFieldsForModel(model);
            setSelectedParamValues(buildPreferredParamValues(fields, buildSelectedParamsFromTaskInfo(taskInfo, model)));
            if (options?.autoCreateAfterFill) {
                pendingAutoCreateAfterMakeSameRef.current = true;
            } else {
                setIsComposerCollapsed(false);
            }
        },
        [loadModelsForSubType, syncCurrentDraftBucketToStore],
    );

    useEffect(() => {
        if (!makeSameIntent) return;
        if (makeSameIntent.mode === "template") return;
        const { postId, autoCreateAfterFill } = makeSameIntent;
        if (postId == null) return;
        void (async () => {
            try {
                await applyMakeSameFromExplore(postId, { autoCreateAfterFill });
            } finally {
                useMakeSameStore.getState().clearPendingMakeSame();
            }
        })();
    }, [makeSameIntent, applyMakeSameFromExplore]);

    /** 任意页面通过 store 请求切换当前产品分类（与 Tab 切换同源，后续由 activeTaskSubType 的 effect 拉模型） */
    useEffect(() => {
        if (!activeTaskSubTypeIntent) return;
        hasExternalInitIntentRef.current = true;
        const { subType } = activeTaskSubTypeIntent;
        if (subType == null) return;
        const latestStore = syncCurrentDraftBucketToStore();
        applyDraftBucketToComposer(subType, latestStore);
        setActiveTaskSubType(subType);
        useActiveTaskSubTypeStore.getState().clearPendingActiveTaskSubType();
        setIsComposerCollapsed(false);
    }, [activeTaskSubTypeIntent, applyDraftBucketToComposer, syncCurrentDraftBucketToStore]);

    /** 消费跨页 applyMedia intent：只订阅 seq，避免 composer 状态变化导致 effect 连环触发 */
    useEffect(() => {
        if (applyMediaIntentSeq === 0) return;
        const intent = useGlobalPromptImageStore.getState().intent;
        if (!intent || intent.seq !== applyMediaIntentSeq) return;
        if (applyMediaIntentProcessingSeqRef.current === applyMediaIntentSeq) return;
        applyMediaIntentProcessingSeqRef.current = applyMediaIntentSeq;
        void applyExternalMediaIntent(intent).finally(() => {
            if (applyMediaIntentProcessingSeqRef.current === applyMediaIntentSeq) {
                applyMediaIntentProcessingSeqRef.current = 0;
            }
            useGlobalPromptImageStore.getState().clearPendingMedia();
        });
    }, [applyMediaIntentSeq, applyExternalMediaIntent]);

    /** 跨页「使用模板」：消费 store → 合成 snapshot → 切 999 → 立刻 clear（避免 Tab 被锁在 999）。 */
    useEffect(() => {
        if (templateSelectIntentSeq === 0) return;
        const intent = useTemplateSelectStore.getState().intent;
        if (!intent || intent.seq !== templateSelectIntentSeq) return;
        const { templateId, images } = intent;
        if (templateId == null || templateId === "") return;

        hasExternalInitIntentRef.current = true;
        syncCurrentDraftBucketToStore();
        const bucket = draftStoreRef.current.draftsBySubType["999"] ?? {};
        activateTemplateComposer(buildTemplateComposerSnapshotFromBucket(bucket, templateId, images));
        useTemplateSelectStore.getState().clearPendingTemplateSelect();
    }, [activateTemplateComposer, syncCurrentDraftBucketToStore, templateSelectIntentSeq]);

    /** 模板类作品制作同款：拉 taskInfo → 合成 snapshot → 切 999（与「使用模板」同源，不用草稿里的旧 templateId）。 */
    useEffect(() => {
        if (makeSameTemplateIntentSeq === 0) return;
        const intent = useMakeSameStore.getState().intent;
        if (!intent || intent.mode !== "template" || intent.seq !== makeSameTemplateIntentSeq) return;
        const { postId, autoCreateAfterFill } = intent;
        if (postId == null) return;

        hasExternalInitIntentRef.current = true;
        let cancelled = false;
        void (async () => {
            try {
                const res: any = await getAssetDetail(postId);
                if (cancelled) return;
                const taskInfo = res?.data?.taskInfo as Record<string, unknown> | undefined;
                syncCurrentDraftBucketToStore();
                const bucket = draftStoreRef.current.draftsBySubType["999"] ?? {};
                const snapshot = buildTemplateComposerSnapshotFromTaskInfo(bucket, taskInfo, {
                    autoCreateAfterFill,
                });
                if (!snapshot) return;
                activateTemplateComposer(snapshot);
            } finally {
                if (!cancelled) useMakeSameStore.getState().clearPendingMakeSame();
            }
        })();
        return () => {
            cancelled = true;
        };
    }, [activateTemplateComposer, makeSameTemplateIntentSeq, syncCurrentDraftBucketToStore]);

    const flushPendingCharacterToReference = useCallback(() => {
        if (activeTaskSubType !== 203) return;
        const payload = pendingReferenceCharacterRef.current;
        if (!payload) return;
        const handler = applyCharacterToReferenceHandlerRef.current;
        if (!handler) return;
        handler(payload);
        pendingReferenceCharacterRef.current = null;
    }, [activeTaskSubType]);

    // 应用角色到参考
    useEffect(() => {
        if (!applyCharacterIntent) return;
        hasExternalInitIntentRef.current = true;
        pendingReferenceCharacterRef.current = applyCharacterIntent.payload;
        const latestStore = syncCurrentDraftBucketToStore();
        applyDraftBucketToComposer(203, latestStore);
        setActiveTaskSubType(203);
        setIsComposerCollapsed(false);
        flushPendingCharacterToReference();
        useCharacterToReferenceStore.getState().clearPendingCharacter();
    }, [applyCharacterIntent, applyDraftBucketToComposer, flushPendingCharacterToReference, syncCurrentDraftBucketToStore]);

    useEffect(() => {
        flushPendingCharacterToReference();
    }, [flushPendingCharacterToReference]);

    // ==================== 参数与费用：字段、预设、尺寸预览、价格 ====================
    /** 当前模型可用参数字段 */
    const modelParamFields = useMemo<ComposerParamField[]>(() => getComposerParamFieldsForModel(activeModelItem), [activeModelItem]);

    /** 参数预设按钮：文案片段 + 竖条分隔 */
    const paramSummary = useMemo(() => {
        const items = modelParamFields
            .map((field) => {
                const selectedValue = selectedParamValues[field.key];
                const option = field.options.find((item) => item.value === selectedValue) ?? field.options[0];
                const label = option?.label ?? String(option?.value ?? "");
                return { key: field.key, label };
            })
            .filter((item) => Boolean(item.label));

        if (items.length === 0) return t("composer.paramPreset");

        return (
            <>
                {items.map((item, index) => (
                    <Fragment key={item.key}>
                        {index > 0 ? (
                            <span className="layoutShell__composerParamPresetDivider" aria-hidden="true" />
                        ) : null}
                        {item.label}
                    </Fragment>
                ))}
            </>
        );
    }, [modelParamFields, selectedParamValues, t]);

    /** 当前比例和分辨率对应尺寸预览 */
    const ratioSizePreview = useMemo(() => {
        const ratioField = modelParamFields.find((field) => isRatioField(field.key, field.name));
        if (!ratioField) return null;
        const selectedValue = selectedParamValues[ratioField.key];
        const fallbackValue = ratioField.options[0]?.value;
        const activeRatio = String(selectedValue ?? fallbackValue ?? "");
        if (!activeRatio) return null;

        // 找到分辨率字段的值，支持动态清晰度
        const resolutionField = modelParamFields.find((field) =>
            field.key.toLowerCase().includes("resolution") || field.name?.toLowerCase().includes("resolution")
        );
        const resolutionValue = resolutionField ? selectedParamValues[resolutionField.key] : undefined;

        return getResolutionSizePreview(activeRatio, resolutionValue);
    }, [modelParamFields, selectedParamValues]);

    /** 已上传视频总时长（秒）。无可用时长时回退为 0。 */
    const inputVideoDurationSeconds = useMemo(() => {
        const total = mediaItems.reduce((sum, item) => {
            if (item.type !== "video") return sum;
            const duration = Number(item.duration);
            if (!Number.isFinite(duration) || duration <= 0) return sum;
            return sum + duration;
        }, 0);
        if (!Number.isFinite(total) || total <= 0) return 0;
        return Math.round(total);
    }, [mediaItems]);

    /** 生成消耗：统一由 useGenerateCost 根据计费策略计算。 */
    const generateCost = useGenerateCost({
        activeModelItem,
        selectedParamValues,
        activeTaskSubType,
        inputVideoDurationSeconds,
        isActionMimicTask,
        selectedActionMimicItem,
    });

    /** 切换模型或字段变化时，保留仍合法的已选参数，其余走默认偏好。 */
    useEffect(() => {
        if (modelParamFields.length === 0) return;
        setSelectedParamValues((prev) => buildPreferredParamValues(modelParamFields, prev));
    }, [activeModelItem, modelParamFields]);

    // ==================== 提交流程：校验、可选扩写、发起生成、跳转刷新 ====================
    /** 点击创建任务 */
    const handleCreateGenTaskClick = useCallback(async () => {
        if (createGenTaskInFlightRef.current) return;
        createGenTaskInFlightRef.current = true;
        setIsCreatingGenTask(true);
        try {
            if (chatPrompt.trim() === "" && !isActionMimicTask) {
                Message.error(t("composer.promptRequired"));
                return;
            }
            const subTypeCfg = getTaskSubTypeRow(activeTaskSubType);
            const minRequired = subTypeCfg?.minUploadImages ?? 0;
            if (minRequired > 0 && mediaItems.length < minRequired) {
                const label = subTypeCfg?.taskNameKey ? t(subTypeCfg.taskNameKey) : t("composer.taskName.reference");
                Message.error(t("composer.minUploadRequired", { label, min: minRequired }));
                return;
            }

            const { uploadImages, uploadVideos, uploadVoice, hasAudio, hasVideo } = splitUploadMediaItems(mediaItems, modelUploadImageLimit);
            const referenceSubjects = referenceStructure.referenceSubjectList;
            const hasReferenceImage = isReferenceTask && referenceSubjects.some((item) => item.type === "image");
            const hasReferenceVideo = isReferenceTask && referenceSubjects.some((item) => item.type === "video");
            const hasImage = uploadImages.length > 0 || hasReferenceImage;
            const hasCompanionVideo =
                hasVideo || hasReferenceVideo || (isActionMimicTask && Boolean(selectedActionMimicItem?.url));
            if (hasAudio && !hasImage && !hasCompanionVideo) {
                Message.error(t("composer.audioRequiresImageOrVideo"));
                return;
            }
            if (isActionMimicTask && !selectedActionMimicItem) {
                Message.error(t("composer.actionMimicExampleRequired", { defaultValue: "请先选择一个动作模仿示例" }));
                return;
            }
            if (!(await ensureSufficientPoints(generateCost))) return;

            const videos = isActionMimicTask
                ? (selectedActionMimicItem?.url ? [selectedActionMimicItem.url] : [])
                : uploadVideos;
            const promptForTask = chatPrompt.trim();
            const payload: Record<string, unknown> = {
                subType: activeTaskSubType,
                ...buildGenTaskModelPayload(activeModelItem),
                type: getTaskSubTypeRow(activeTaskSubType)?.type,
                ...selectedParamValues,
                ...dimensionPayloadFromPreview(selectedParamValues.resolution, ratioSizePreview),
                chatPrompt: promptForTask,
                images: uploadImages,
                videos,
                voice: uploadVoice,
            };
            if (isActionMimicTask) {
                payload.templateVideoMaterialId = selectedActionMimicItem?.type === "官方示例" ? selectedActionMimicItem.id : undefined;
                if (payload.templateVideoMaterialId == null) delete payload.templateVideoMaterialId;
            }
            if (isReferenceTask) {
                payload.referenceSubjectList = referenceStructure.referenceSubjectList;
            }

            if (uploadImages.length <= 0) delete payload.images;
            if (videos.length <= 0) delete payload.videos;
            if (uploadVoice.length <= 0) delete payload.voice;

            try {
                Message.success(t("composer.taskCreating"));
                await createGenTask(payload);
                closeAssetDetailsModal();
                void setUser();
                const taskType = getTaskSubTypeRow(activeTaskSubType)?.type ?? 1;
                const onMyCreations = currentPathname === ROUTES.CREATE_MY_CREATIONS;
                const currentType = Number(new URLSearchParams(currentSearch).get("type"));
                const isSameTypeOnMyCreations = onMyCreations && Number.isFinite(currentType) && currentType === taskType;
                // 已在「我的创造」且 type 一致时，发事件让列表静默刷新，避免整页重拉。
                if (isSameTypeOnMyCreations) {
                    window.dispatchEvent(new CustomEvent("myCreations:taskCreated", { detail: { type: taskType } }));
                } else {
                    const nextSearch = new URLSearchParams();
                    nextSearch.set("type", String(taskType));
                    nextSearch.set(MY_CREATIONS_REFRESH_QUERY, String(Date.now()));
                    navigateToMyCreations(
                        { pathname: ROUTES.CREATE_MY_CREATIONS, search: `?${nextSearch.toString()}` },
                        // 已在「我的创造」时用 replace，避免历史记录被同一页面刷屏。
                        { replace: onMyCreations },
                    );
                }

            } catch {
                /* 失败时 request 拦截器已提示 */
            }
        } finally {
            createGenTaskInFlightRef.current = false;
            setIsCreatingGenTask(false);
        }
    }, [
        navigateToMyCreations,
        currentPathname,
        currentSearch,
        activeTaskSubType,
        activeModelItem,
        selectedParamValues,
        mediaItems,
        modelUploadImageLimit,
        chatPrompt,
        ratioSizePreview,
        referenceStructure,
        isActionMimicTask,
        selectedActionMimicItem,
        generateCost,
        t,
    ]);

    useEffect(() => {
        if (!pendingAutoCreateAfterMakeSameRef.current) return;
        if (createGenTaskInFlightRef.current) return;
        if (activeTaskSubType == null || activeModelItem == null) return;
        if (activeTaskSubType === 205 && !selectedActionMimicItem) return;
        pendingAutoCreateAfterMakeSameRef.current = false;
        void handleCreateGenTaskClick();
    }, [activeModelItem, activeTaskSubType, handleCreateGenTaskClick, selectedActionMimicItem]);

    // ==================== 草稿持久化：回填、模板同步、防抖保存 ====================
    /** 切换模型后，超出 `uploadImageLimit` 的图片从列表中移除。 */
    useEffect(() => {
        if (modelUploadImageLimit == null) return;
        setMediaItems((prev) => {
            let imageCount = 0;
            const next = prev.filter((item) => {
                if (item.type !== "image") return true;
                if (imageCount >= modelUploadImageLimit) return false;
                imageCount += 1;
                return true;
            });
            return next.length === prev.length ? prev : next;
        });
    }, [activeModelItem?.id, modelUploadImageLimit]);
    // 页面进入时回填用户上次编辑草稿
    useEffect(() => {
        let cancelled = false;
        void getUserOperationDetail({})
            .then((res: any) => {
                if (cancelled) return;
                const dataRaw = res?.data?.data ?? "";
                const store =
                    parseDraftStore(typeof dataRaw === "string" ? dataRaw : String(dataRaw ?? "")) ?? {
                        version: 2 as const,
                        currentProductType: null,
                        draftsBySubType: {},
                    };
                draftStoreRef.current = store;
                latestSavedDraftStoreRef.current = JSON.stringify(store);
                if (hasExternalInitIntentRef.current) return;
                applyDraftBucketToComposer(store.currentProductType, store);
                if (store.currentProductType != null) {
                    setActiveTaskSubType(store.currentProductType);
                }
            })
            .catch(() => { })
            .finally(() => {
                hasLoadedUserOperationRef.current = true;
            });
        return () => {
            cancelled = true;
        };
    }, [applyDraftBucketToComposer]);

    // 只有草稿发生改变才保存，并做防抖
    useEffect(() => {
        if (!hasLoadedUserOperationRef.current) return;
        const nextStore = syncCurrentDraftBucketToStore();
        const snapshot = JSON.stringify(nextStore);
        if (snapshot === latestSavedDraftStoreRef.current) return;
        const timer = window.setTimeout(() => {
            const freshStore = syncCurrentDraftBucketToStoreRef.current();
            const nextSnapshot = JSON.stringify(freshStore);
            if (nextSnapshot === latestSavedDraftStoreRef.current) return;
            void saveUserOperation({
                data: nextSnapshot,
            })
                .then(() => {
                    latestSavedDraftStoreRef.current = nextSnapshot;
                })
                .catch(() => {
                    /* 保存失败时忽略，后续变更会继续尝试 */
                });
        }, GLOBAL_PROMPT_SAVE_DEBOUNCE_MS);
        return () => {
            window.clearTimeout(timer);
        };
    }, [syncCurrentDraftBucketToStore, templateDraftSnapshot]);

    const handleTaskSubTypeChange = useCallback((nextTaskSubType: number) => {
        const currentTemplateSnapshot = isTemplateTask ? (templateComposerRef.current?.getSnapshot() ?? templateDraftRef.current) : undefined;

        if (currentTemplateSnapshot) {
            setTemplateDraftSnapshot(currentTemplateSnapshot);
        }
        const latestStore = syncCurrentDraftBucketToStore({ templateSnapshot: currentTemplateSnapshot });
        applyDraftBucketToComposer(nextTaskSubType, latestStore);
        setActiveTaskSubType(nextTaskSubType);
        if (nextTaskSubType !== 203) {
            pendingReferenceCharacterRef.current = null;
        }
    }, [applyDraftBucketToComposer, isTemplateTask, syncCurrentDraftBucketToStore]);

    const handleApplyCharacterToReferenceHandlerRegister = useCallback(
        ((handler: ((payload: { id: number | string; title: string; threeViewImage: string }) => void) | null) => {
            applyCharacterToReferenceHandlerRef.current = handler;
            flushPendingCharacterToReference();
        }),
        [flushPendingCharacterToReference],
    );

    const isEmbedded = mode === "embedded";

    // ==================== 视图结构：头部标签、主体输入、底部参数与操作 ====================
    return (
        // 全局创作输入区容器
        <section
            className={classNames(
                "layoutShell__composer",
                isComposerCollapsed && "layoutShell__composer_collapsed",
                isEmbedded && "layoutShell__composer_embedded",
                isEmbedded && embeddedVisible && "layoutShell__composer_embeddedVisible",
            )}
            aria-label={t("composer.aria.userInputPanel")}
        >
            {/* 顶部区域：模式标签 + 头部操作 */}
            <div className="layoutShell__composerChrome layoutShell__composerChrome--header">
                <div className="layoutShell__composerHeader">
                    {/* 模式标签组 */}
                    <div
                        ref={composerTabsWheelRef}
                        className="layoutShell__composerTabs"
                        role="tablist"
                        aria-label={t("composer.aria.modeTabs")}
                    >
                        {aiCategories.map((item, idx) => {
                            const isActive = activeTaskSubType === item.taskSubType;
                            return (
                                <button
                                    key={`${String(item.taskSubType ?? item.id ?? "category")}-${idx}`}
                                    type="button"
                                    className={classNames("layoutShell__composerTab", isActive && "layoutShell__composerTab_active")}
                                    role="tab"
                                    aria-selected={isActive}
                                    data-task-sub-type={item.taskSubType}
                                    onClick={() => handleTaskSubTypeChange(item.taskSubType)}
                                >
                                    {typeof item.icon === "string" && item.icon ? (
                                        <img
                                            src={
                                                /^https?:\/\//i.test(item.icon) || item.icon.startsWith("data:")
                                                    ? item.icon
                                                    : new URL(item.icon, import.meta.url).href
                                            }
                                        />
                                    ) : null}
                                    <span>{item.taskSubType === 999 ? t("composer.tabs.template") : item.name}</span>
                                </button>
                            );
                        })}
                    </div>
                    {/* 头部操作按钮组 */}
                    <div className="layoutShell__composerHeaderActions">
                        <div className="layoutShell__composerHeaderActions__aiGenerated">
                            {t("pages.assetDetails.ai_generated")}
                        </div>
                        <Tooltip content={t("layout.usageTutorial")}>
                            <button
                                type="button"
                                className="layoutShell__composerIconBtn"
                                onClick={() => {
                                    if (usageTutorialPageUrl) window.open(usageTutorialPageUrl, "_blank");
                                }}
                            >
                                <img style={{ transform: "none" }} src={COMPOSER_ASSET.icHelp} />
                            </button>
                        </Tooltip>
                        <Tooltip content={t("composer.actions.collapsePanel")}>
                            <button
                                type="button"
                                className="layoutShell__composerIconBtn"
                                aria-label={t("composer.actions.collapsePanel")}
                                aria-expanded={!isComposerCollapsed}
                                onClick={requestComposerCollapse}
                            >
                                <img src={COMPOSER_ASSET.icArrowRightGray} />
                            </button>
                        </Tooltip>
                    </div>
                </div>
            </div>
            {isTemplateTask ? (
                <GlobalTemplateComposerShell
                    ref={templateComposerRef}
                    isComposerCollapsed={isComposerCollapsed}
                    onExpandFromCollapsedBody={handleComposerChromeExpand}
                    initialSnapshot={templateDraftSnapshot}
                    onAppendImagesHandlerRegister={handleTemplateApplyMediaHandlerRegister}
                    onSnapshotChange={setTemplateDraftSnapshot}
                    pathname={currentPathname}
                    navigateTo={navigateToMyCreations}
                />
            ) : (
                <>
                    {/* 中间区域：上传入口与提示 */}
                    <div
                        ref={composerBodyWheelRef}
                        className="layoutShell__composerBody"
                        onClickCapture={(evt) => {
                            if (!isComposerCollapsed) return;
                            setIsComposerCollapsed(false);
                            evt.preventDefault();
                            evt.stopPropagation();
                        }}
                        onDragOver={(evt) => {
                            evt.preventDefault();
                            evt.stopPropagation();
                        }}
                        onDragLeave={(evt) => {
                            evt.preventDefault();
                            evt.stopPropagation();
                        }}
                        onDrop={handleUploadAreaDrop}
                    >
                        {isHeadTailTask ? (
                            <HeadTailUploadPromptSection
                                mediaItems={mediaItems}
                                uploadAccept={uploadAccept}
                                pendingHeadTailSlotRef={pendingHeadTailSlotRef}
                                chatPrompt={chatPrompt}
                                onRemoveMedia={handleRemoveMedia}
                                onUploadInputChange={handleUploadInputChange}
                                onChatPromptChange={handleChatPromptChange}
                            />
                        ) : isReferenceTask ? (
                            <ReferenceUploadPromptSection
                                visibleMediaItems={mediaItems}
                                modelUploadImageLimit={modelUploadImageLimit}
                                allowNonImageUpload={allowNonImageUpload}
                                composerMediaUploadEnabled={composerMediaUploadEnabled}
                                uploadAccept={uploadAccept}
                                chatPrompt={chatPrompt}
                                onRemoveMedia={handleRemoveMedia}
                                onRestoreSubjectImages={handleRestoreSubjectImages}
                                onReferenceStructureChange={setReferenceStructure}
                                onApplyCharacterToReferenceHandlerRegister={handleApplyCharacterToReferenceHandlerRegister}
                                onUploadInputChange={handleUploadInputChange}
                                onChatPromptChange={handleChatPromptChange}
                            />
                        ) : isActionMimicTask ? (
                            <ActionMimicUploadPromptSection
                                visibleMediaItems={mediaItems}
                                modelUploadImageLimit={modelUploadImageLimit}
                                allowNonImageUpload={allowNonImageUpload}
                                composerMediaUploadEnabled={composerMediaUploadEnabled}
                                uploadAccept={uploadAccept}
                                chatPrompt={chatPrompt}
                                placeholder={taskSubTypePlaceholder}
                                onRemoveMedia={handleRemoveMedia}
                                onUploadInputChange={handleUploadInputChange}
                                onChatPromptChange={handleChatPromptChange}
                                onSelectedExampleItemChange={setSelectedActionMimicItem}
                                templateVideoMaterialId={templateVideoMaterialId}
                            />
                        ) : (
                            <CommonUploadPromptSection
                                visibleMediaItems={mediaItems}
                                modelUploadImageLimit={modelUploadImageLimit}
                                allowNonImageUpload={allowNonImageUpload}
                                composerMediaUploadEnabled={composerMediaUploadEnabled}
                                uploadAccept={uploadAccept}
                                chatPrompt={chatPrompt}
                                placeholder={taskSubTypePlaceholder}
                                onRemoveMedia={handleRemoveMedia}
                                onUploadInputChange={handleUploadInputChange}
                                onChatPromptChange={handleChatPromptChange}
                            />
                        )}
                    </div>
                    {/* 底部区域：参数与主操作 */}
                    <div className="layoutShell__composerChrome layoutShell__composerChrome--footer">
                        <div className="layoutShell__composerFooter">
                            {/* 参数与模型选择 */}
                            <div className="layoutShell__composerChips" >
                                {/* 参数预设下拉 */}
                                <Dropdown
                                    trigger="click"
                                    position="tl"
                                    popupVisible={paramMenuOpen}
                                    onVisibleChange={setParamMenuOpen}
                                    getPopupContainer={getDropdownPopupContainer}
                                    triggerProps={{ className: "layoutShell__composerParamDropdownTrigger" }}
                                    droplist={
                                        <div className="layoutShell__composerParamDropdown" role="menu" aria-label={t("composer.aria.paramPresetList")}>
                                            {modelParamFields.map((field) => {
                                                const selectedValue = selectedParamValues[field.key];
                                                const fallbackValue = field.options[0]?.value;
                                                const activeValue = selectedValue ?? fallbackValue;
                                                const ratioField = isRatioField(field.key, field.name);
                                                return (
                                                    <div key={field.key} className="layoutShell__composerParamGroup">
                                                        <p className="layoutShell__composerParamTitle">{field.key === "batchSize" ? t("composer.fields.batchSize") : field.name}</p>
                                                        <div
                                                            className={classNames(
                                                                "layoutShell__composerParamOptions",
                                                                ratioField && "layoutShell__composerParamOptions_ratio",
                                                            )}
                                                            role="group"
                                                            aria-label={field.name}
                                                        >
                                                            {field.options.map((option, idx) => {
                                                                const optionValue = option.value;
                                                                const optionLabel = option.label ?? String(optionValue);
                                                                const isActive = activeValue === optionValue;
                                                                const ratioPreview = ratioField ? ratioChipPreviewSize(optionLabel) : null;
                                                                return (
                                                                    <button
                                                                        key={`${field.key}-${String(optionValue)}-${idx}`}
                                                                        type="button"
                                                                        role="menuitemradio"
                                                                        aria-checked={isActive}
                                                                        className={classNames(
                                                                            "layoutShell__composerParamOption",
                                                                            isActive && "layoutShell__composerParamOption_active",
                                                                            ratioField && "layoutShell__composerParamOption_ratio",
                                                                        )}
                                                                        onClick={() => {
                                                                            setSelectedParamValues((prev) => ({ ...prev, [field.key]: optionValue }));
                                                                        }}
                                                                    >
                                                                        {ratioField ? (
                                                                            <>
                                                                                <span className="layoutShell__composerRatioShapeSlot" aria-hidden="true">
                                                                                    <span
                                                                                        className="layoutShell__composerRatioShape"
                                                                                        style={{
                                                                                            width: `${ratioPreview?.width ?? 20}px`,
                                                                                            height: `${ratioPreview?.height ?? 20}px`,
                                                                                        }}
                                                                                    />
                                                                                </span>
                                                                                <span className="layoutShell__composerRatioLabel">{optionLabel}</span>
                                                                            </>
                                                                        ) : (
                                                                            optionLabel
                                                                        )}
                                                                    </button>
                                                                );
                                                            })}
                                                        </div>
                                                        {ratioField && ratioSizePreview ? (
                                                            <div className="layoutShell__composerParamRatioSizeWrap" aria-label={t("composer.aria.sizePreview")}>
                                                                <p className="layoutShell__composerParamTitle">{t("composer.fields.size")}</p>
                                                                <div className="layoutShell__composerParamRatioSizeRow">
                                                                    <span className="layoutShell__composerParamRatioSizeValue">W {ratioSizePreview.width}</span>
                                                                    <img
                                                                        className="layoutShell__composerParamRatioSizeLink"
                                                                        src={COMPOSER_ASSET.icLink}
                                                                    />
                                                                    <span className="layoutShell__composerParamRatioSizeValue">H {ratioSizePreview.height}</span>
                                                                </div>
                                                            </div>
                                                        ) : null}
                                                    </div>
                                                );
                                            })}
                                        </div>
                                    }
                                >
                                    <button type="button" className="layoutShell__composerParamPreset" aria-label={t("composer.paramPreset")}>
                                        <span className="layoutShell__composerParamPresetItem">{paramSummary}</span>
                                    </button>
                                </Dropdown>

                                {/* 模型选择下拉 */}
                                <Dropdown
                                    trigger="click"
                                    position="tl"
                                    popupVisible={modelMenuOpen}
                                    onVisibleChange={setModelMenuOpen}
                                    getPopupContainer={getDropdownPopupContainer}
                                    triggerProps={{ className: "layoutShell__composerModelDropdownTrigger" }}
                                    droplist={
                                        <div className="layoutShell__composerModelDropdown" role="menu" aria-label={t("composer.modelLibrary")}>
                                            <p className="layoutShell__composerModelDropdownTitle">{t("composer.modelLibrary")}</p>
                                            <div className="layoutShell__composerModelDropdownList">
                                                {aiModels.map((item, idx) => {
                                                    const isActive = activeModelItem === item;
                                                    return (
                                                        <button
                                                            key={item.id ?? idx}
                                                            type="button"
                                                            role="menuitem"
                                                            className={classNames(
                                                                "layoutShell__composerModelDropdownItem",
                                                                isActive && "layoutShell__composerModelDropdownItem_active",
                                                            )}
                                                            onClick={() => {
                                                                setActiveModelItem(item);
                                                                setModelMenuOpen(false);
                                                                setSelectedParamValues(
                                                                    buildPreferredParamValues(getComposerParamFieldsForModel(item), {}),
                                                                );
                                                            }}
                                                        >
                                                            <img
                                                                className="layoutShell__composerModelDropdownItemIcon"
                                                                src={
                                                                    typeof item.icon === "string" &&
                                                                        item.icon &&
                                                                        (/^https?:\/\//i.test(item.icon) || item.icon.startsWith("data:"))
                                                                        ? item.icon
                                                                        : typeof item.icon === "string" && item.icon
                                                                            ? new URL(item.icon, import.meta.url).href
                                                                            : COMPOSER_ASSET.icModel
                                                                }
                                                            />
                                                            <span className="layoutShell__composerModelDropdownItemText">
                                                                <span className="layoutShell__composerModelDropdownItemName">{item.name}</span>
                                                                <span className="layoutShell__composerModelDropdownItemDesc">{item.description}</span>
                                                            </span>
                                                        </button>
                                                    );
                                                })}
                                            </div>
                                        </div>
                                    }
                                >
                                    <button
                                        type="button"
                                        className="layoutShell__composerModelChip"
                                        aria-haspopup="menu"
                                        aria-expanded={modelMenuOpen}
                                        aria-label={t("composer.actions.selectModel")}
                                    >
                                        <img src={COMPOSER_ASSET.icModel} />
                                        <span>{activeModelItem?.name ?? ""}</span>
                                    </button>
                                </Dropdown>
                            </div>
                            {/* 重置与消耗展示 */}
                            <div className="layoutShell__composerActions">
                                {/* <Tooltip content={t("composer.actions.reset")}> */}
                                <button
                                    type="button"
                                    className="layoutShell__composerResetBtn"
                                    aria-label={t("composer.actions.reset")}
                                    onClick={handleResetComposer}
                                >
                                    <img src={COMPOSER_ASSET.icRefresh} alt="" />
                                    <span>{t("composer.actions.reset")}</span>
                                </button>
                                {/* </Tooltip> */}
                                <button
                                    type="button"
                                    className="layoutShell__composerCostBtn"
                                    disabled={isCreatingGenTask}
                                    aria-busy={isCreatingGenTask}
                                    onClick={handleCreateGenTaskClick}
                                >
                                    <img src={COMPOSER_ASSET.icStarsWhite} />
                                    <span>{generateCost}</span>
                                </button>
                            </div>
                        </div>
                    </div>
                </>
            )}
        </section>
    );
}
