"use client";

import { useEffect, useMemo, useRef, useState, type KeyboardEvent, type ReactNode, type RefObject } from "react";
import { Check, Download, ExternalLink, Film, ImageIcon, ImagePlus, ImageUp, Loader2, Music, Paintbrush, Pencil, Plus, RefreshCw, Save, Send, Upload, X } from "lucide-react";
import clsx from "clsx";
import { ImageEditor, type ImageEditMode } from "@/components/image-editor";
import { clampPage, pageItems, Pagination } from "@/components/pagination";
import { crossfadeIn, pulseFeedback, revealChildren, runScopedMotion } from "@/lib/ui/motion";
import { VIDEO_DURATION_DEFAULT, VIDEO_DURATION_OPTIONS, VIDEO_RATIOS, VIDEO_RESOLUTIONS, clampVideoDuration } from "@/lib/video-settings";
import type { Asset, GenerationJob } from "@/lib/types";
import type { PromptMaterial } from "@/lib/prompt/assembler";
import {
  detectMaterialDraftStart,
  insertMaterialDraftToken,
  nextMaterialDraftToken,
  normalizeMaterialDraftToken,
  type MaterialDraftKind
} from "@/lib/prompt/material-draft";
import { extractMaterialPlaceholders } from "@/lib/prompt/material-placeholders";

type GenerateMode = "image" | "video";
type StudioMode = GenerateMode | ImageEditMode;
type MaterialKind = PromptMaterial["type"];
type ImageGenerateEngine = "jimeng" | "evolink";

type MentionState = {
  start: number;
  query: string;
};

type MaterialDraftTarget = "prompt" | "template";

type MaterialDraftState = {
  target: MaterialDraftTarget;
  index: number;
  query: string;
  activeIndex: number | null;
};

type MaterialDraftOption = {
  token: string;
  label: string;
  caption: string;
  type: MaterialKind;
};

type HealthCapability = {
  id: string;
  engine?: string;
};

type ImageTemplate = {
  id: string;
  name: string;
  prompt: string;
  previewImageUrl?: string;
  settings: {
    engine?: ImageGenerateEngine;
    width?: number;
    height?: number;
    forceSingle?: boolean;
    scale?: number;
    quality?: string;
  };
  sortOrder: number;
  updatedAt?: string;
};

type TemplateForm = {
  name: string;
  prompt: string;
  previewImageUrl: string;
  engine: ImageGenerateEngine;
  jimengInfluence: string;
  evolinkQuality: string;
  size: string;
  sortOrder: string;
};

const jimengInfluenceOptions = [
  { id: "creative", label: "创意 35", scale: 35 },
  { id: "balanced", label: "均衡 50", scale: 50 },
  { id: "precise", label: "贴合 70", scale: 70 },
  { id: "strict", label: "严格 85", scale: 85 }
];

const evolinkQualityOptions = [
  { id: "low", label: "快速", quality: "low" },
  { id: "medium", label: "标准", quality: "medium" },
  { id: "high", label: "精细", quality: "high" }
];

const imageSizePresets = [
  { label: "1:1", width: 2048, height: 2048 },
  { label: "4:3", width: 2304, height: 1728 },
  { label: "16:9", width: 2560, height: 1440 },
  { label: "9:16", width: 1440, height: 2560 }
];

type ImageSizePreset = (typeof imageSizePresets)[number];

type TemplateConsoleSnapshot = {
  mode: GenerateMode;
  imagePrompt: string;
  imageSize: ImageSizePreset;
  imageEngine: ImageGenerateEngine;
  jimengInfluence: string;
  evolinkQuality: string;
};

const materialUploadAccept = "image/*,video/*,audio/*";
const templatePreviewAccept = "image/*";
const MATERIAL_PAGE_SIZE = 6;
const defaultTemplateForm: TemplateForm = {
  name: "",
  prompt: "",
  previewImageUrl: "",
  engine: "jimeng",
  jimengInfluence: jimengInfluenceOptions[1].id,
  evolinkQuality: evolinkQualityOptions[1].id,
  size: imageSizePresets[0].label,
  sortOrder: "0"
};

export function CreateStudio({ initialMode = "image" }: { initialMode?: StudioMode }) {
  const [mode, setMode] = useState<StudioMode>(initialMode);
  const [promptByMode, setPromptByMode] = useState<Record<GenerateMode, string>>({
    image: "",
    video: ""
  });
  const [materials, setMaterials] = useState<PromptMaterial[]>([]);
  const [imageTemplates, setImageTemplates] = useState<ImageTemplate[]>([]);
  const [taskAssets, setTaskAssets] = useState<Asset[]>([]);
  const [recentJobs, setRecentJobs] = useState<GenerationJob[]>([]);
  const [templatesLoading, setTemplatesLoading] = useState(false);
  const [tasksLoading, setTasksLoading] = useState(false);
  const [templateSaving, setTemplateSaving] = useState(false);
  const [templatePreviewUploading, setTemplatePreviewUploading] = useState(false);
  const [templateError, setTemplateError] = useState<string | null>(null);
  const [tasksError, setTasksError] = useState<string | null>(null);
  const [activeTemplateId, setActiveTemplateId] = useState<string | null>(null);
  const [templateEditorOpen, setTemplateEditorOpen] = useState(false);
  const [editingTemplateId, setEditingTemplateId] = useState<string | null>(null);
  const [previewTemplate, setPreviewTemplate] = useState<ImageTemplate | null>(null);
  const [previewTaskAsset, setPreviewTaskAsset] = useState<Asset | null>(null);
  const [durationNow, setDurationNow] = useState(() => Date.now());
  const [templateForm, setTemplateForm] = useState<TemplateForm>(defaultTemplateForm);
  const [templateRestoreState, setTemplateRestoreState] = useState<TemplateConsoleSnapshot | null>(null);
  const [busy, setBusy] = useState(false);
  const [uploading, setUploading] = useState(false);
  const [error, setError] = useState<string | null>(null);
  const [notice, setNotice] = useState<string | null>(null);
  const [imageSize, setImageSize] = useState(imageSizePresets[0]);
  const [imageEngine, setImageEngine] = useState<ImageGenerateEngine>("jimeng");
  const [jimengInfluence, setJimengInfluence] = useState(jimengInfluenceOptions[1].id);
  const [evolinkQuality, setEvolinkQuality] = useState(evolinkQualityOptions[1].id);
  const [videoRatio, setVideoRatio] = useState("9:16");
  const [videoDuration, setVideoDuration] = useState(VIDEO_DURATION_DEFAULT);
  const [videoResolution, setVideoResolution] = useState("720p");
  const [mentionState, setMentionState] = useState<MentionState | null>(null);
  const [materialDraft, setMaterialDraft] = useState<MaterialDraftState | null>(null);
  const [activeMentionIndex, setActiveMentionIndex] = useState<number | null>(null);
  const [promptScrollTop, setPromptScrollTop] = useState(0);
  const [templatePromptScrollTop, setTemplatePromptScrollTop] = useState(0);
  const [materialPage, setMaterialPage] = useState(1);
  const studioRef = useRef<HTMLDivElement | null>(null);
  const modePanelRef = useRef<HTMLDivElement | HTMLElement | null>(null);
  const feedbackRef = useRef<HTMLDivElement | null>(null);
  const materialBoardRef = useRef<HTMLDivElement | null>(null);
  const promptRef = useRef<HTMLTextAreaElement | null>(null);
  const templatePromptRef = useRef<HTMLTextAreaElement | null>(null);
  const materialDraftInputRef = useRef<HTMLInputElement | null>(null);

  const isImageEditMode = mode === "inpaint" || mode === "upscale";
  const generateMode: GenerateMode = mode === "video" ? "video" : "image";
  const prompt = promptByMode[generateMode];
  const selectedJimengInfluence = jimengInfluenceOptions.find((option) => option.id === jimengInfluence) || jimengInfluenceOptions[1];
  const selectedEvolinkQuality = evolinkQualityOptions.find((option) => option.id === evolinkQuality) || evolinkQualityOptions[1];
  const selectedTemplateJimengInfluence = jimengInfluenceOptions.find((option) => option.id === templateForm.jimengInfluence) || jimengInfluenceOptions[1];
  const selectedTemplateEvolinkQuality = evolinkQualityOptions.find((option) => option.id === templateForm.evolinkQuality) || evolinkQualityOptions[1];
  const templateFormReady = Boolean(templateForm.name.trim() && templateForm.prompt.trim() && templateForm.previewImageUrl.trim());
  const templateFormPlaceholders = useMemo(() => extractMaterialPlaceholders(templateForm.prompt), [templateForm.prompt]);
  const materialPlaceholders = useMemo(() => extractMaterialPlaceholders(prompt), [prompt]);
  const referencedMaterialLabels = useMemo(() => new Set(materialPlaceholders.map((placeholder) => placeholder.token)), [materialPlaceholders]);
  const missingMaterialPlaceholders = useMemo(() => {
    const existing = new Set(materials.map((material) => normalizeMaterialLabel(material.label)).filter((label): label is string => Boolean(label)));
    return materialPlaceholders.filter((placeholder) => !existing.has(placeholder.token));
  }, [materialPlaceholders, materials]);
  const submitDisabled = busy || !prompt.trim() || missingMaterialPlaceholders.length > 0;
  const submitTitle = missingMaterialPlaceholders.length
    ? `请先上传 ${missingMaterialPlaceholders.map((placeholder) => placeholder.token).join("、")}`
    : generateMode === "image" ? "生成图片" : "生成视频";
  const visibleMaterials = pageItems(materials, materialPage, MATERIAL_PAGE_SIZE);
  const materialPageOffset = (clampPage(materialPage, materials.length, MATERIAL_PAGE_SIZE) - 1) * MATERIAL_PAGE_SIZE;
  const mentionSuggestions = useMemo(() => {
    if (!mentionState) return [];
    return materials.filter((material) => materialMatchesMention(material, mentionState.query)).slice(0, 8);
  }, [materials, mentionState]);
  const materialDraftOptions = useMemo(() => {
    if (!materialDraft) return [];
    return buildMaterialDraftOptions({
      target: materialDraft.target,
      query: materialDraft.query,
      prompt: materialDraft.target === "template" ? templateForm.prompt : prompt,
      materials
    });
  }, [materialDraft, materials, prompt, templateForm.prompt]);
  const taskAssetById = useMemo(() => {
    const map = new Map<string, Asset>();
    for (const asset of taskAssets) map.set(asset.id, asset);
    return map;
  }, [taskAssets]);
  const hasLiveTasks = useMemo(() => recentJobs.some((job) => !isTerminalStatus(job.status)), [recentJobs]);

  useEffect(() => {
    setActiveMentionIndex(null);
  }, [mentionState?.query, mentionSuggestions.length]);

  useEffect(() => {
    if (!materialDraft) return;
    window.requestAnimationFrame(() => materialDraftInputRef.current?.focus());
  }, [materialDraft?.target, materialDraft?.index]);

  useEffect(() => {
    setMaterialPage((page) => clampPage(page, materials.length, MATERIAL_PAGE_SIZE));
  }, [materials.length]);

  useEffect(() => {
    return runScopedMotion(studioRef, (scope) => revealChildren(scope));
  }, []);

  useEffect(() => {
    crossfadeIn(modePanelRef.current);
  }, [mode]);

  useEffect(() => {
    pulseFeedback(feedbackRef.current);
  }, [error, notice]);

  useEffect(() => {
    if (materialBoardRef.current) revealChildren(materialBoardRef.current, ".material-card");
  }, [materials.length, materialPage]);

  useEffect(() => {
    let active = true;
    void fetch("/api/health")
      .then((response) => response.ok ? response.json() : null)
      .then((payload: { capabilities?: HealthCapability[] } | null) => {
        const engine = payload?.capabilities?.find((capability) => capability.id === "image.generate")?.engine;
        if (active && (engine === "evolink" || engine === "jimeng")) setImageEngine(engine);
      })
      .catch(() => undefined);
    return () => {
      active = false;
    };
  }, []);

  useEffect(() => {
    let active = true;
    void loadImageTemplates(() => active);
    return () => {
      active = false;
    };
  }, []);

  useEffect(() => {
    let active = true;
    void loadTaskModuleData(() => active);
    const timer = window.setInterval(() => {
      if (document.visibilityState === "visible") {
        void loadTaskModuleData(() => active, { silent: true });
      }
    }, 15000);
    return () => {
      active = false;
      window.clearInterval(timer);
    };
  }, []);

  useEffect(() => {
    if (!hasLiveTasks) return undefined;
    setDurationNow(Date.now());
    const timer = window.setInterval(() => {
      setDurationNow(Date.now());
    }, 1000);
    return () => window.clearInterval(timer);
  }, [hasLiveTasks]);

  async function loadImageTemplates(isActive: () => boolean = () => true) {
    setTemplatesLoading(true);
    setTemplateError(null);
    try {
      const response = await fetch("/api/image-templates", { cache: "no-store" });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || "读取模板失败");
      const nextTemplates = ((payload.templates || []) as ImageTemplate[]).sort(sortTemplates);
      if (isActive()) {
        setImageTemplates(nextTemplates);
      }
    } catch (err) {
      if (isActive()) setTemplateError(err instanceof Error ? err.message : String(err));
    } finally {
      if (isActive()) setTemplatesLoading(false);
    }
  }

  async function loadTaskModuleData(isActive: () => boolean = () => true, options: { silent?: boolean } = {}) {
    if (!options.silent) setTasksLoading(true);
    setTasksError(null);
    try {
      const [assetResponse, imageResponse, videoResponse] = await Promise.all([
        fetch("/api/assets", { cache: "no-store" }),
        fetch("/api/generations/image", { cache: "no-store" }),
        fetch("/api/generations/video", { cache: "no-store" })
      ]);
      const [assetPayload, imagePayload, videoPayload] = await Promise.all([
        assetResponse.json(),
        imageResponse.json(),
        videoResponse.json()
      ]);
      if (!assetResponse.ok) throw new Error(assetPayload.error || "读取资产失败");
      if (!imageResponse.ok) throw new Error(imagePayload.error || "读取图片任务失败");
      if (!videoResponse.ok) throw new Error(videoPayload.error || "读取视频任务失败");
      const dedupedJobs = new Map<string, GenerationJob>();
      for (const job of [...(imagePayload.jobs || []), ...(videoPayload.jobs || [])] as GenerationJob[]) {
        dedupedJobs.set(job.id, job);
      }
      const nextJobs = [...dedupedJobs.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
      if (isActive()) {
        setTaskAssets((assetPayload.assets || []) as Asset[]);
        setRecentJobs(nextJobs);
      }
    } catch (err) {
      if (isActive()) setTasksError(err instanceof Error ? err.message : String(err));
    } finally {
      if (isActive() && !options.silent) setTasksLoading(false);
    }
  }

  function setPrompt(value: string) {
    setPromptByMode((items) => ({ ...items, [generateMode]: value }));
  }

  function handlePromptInput(value: string, cursor: number) {
    const draft = detectMaterialDraftStart(prompt, value, cursor);
    if (draft) {
      setPrompt(draft.text);
      startMaterialDraft("prompt", draft.index);
      return;
    }
    setPrompt(value);
    updateMention(value, cursor);
  }

  function updateMention(value: string, cursor: number) {
    setMentionState(findMentionAtCursor(value, cursor));
  }

  async function uploadAssetFiles(files: File[]): Promise<Asset[]> {
    const formData = new FormData();
    for (const file of files) formData.append("files", file);
    const response = await fetch("/api/assets/upload", {
      method: "POST",
      body: formData
    });
    const payload = await response.json();
    if (!response.ok) throw new Error(payload.error || "上传失败");
    return payload.assets as Asset[];
  }

  async function uploadFiles(files: FileList | null, options: { targetLabel?: string; expectedType?: MaterialKind } = {}) {
    const selectedFiles = Array.from(files || []);
    const expectedType = options.expectedType;
    if (!selectedFiles.length) return;
    if (expectedType && selectedFiles.some((file) => !fileMatchesMaterialKind(file, expectedType))) {
      setError(`请上传${shortTypeName(expectedType)}素材。`);
      return;
    }
    setUploading(true);
    setError(null);
    setNotice(null);
    try {
      const uploaded = await uploadAssetFiles(selectedFiles);
      if (expectedType && uploaded.some((asset) => materialTypeForAsset(asset) !== expectedType)) {
        throw new Error(`请上传${shortTypeName(expectedType)}素材。`);
      }
      setMaterials((items) => {
        const next = [...items];
        uploaded.forEach((asset, index) => {
          const targetLabel = index === 0 ? normalizeMaterialLabel(options.targetLabel) : undefined;
          const material = materialFromAsset(asset, next, targetLabel);
          const existingIndex = targetLabel ? next.findIndex((item) => normalizeMaterialLabel(item.label) === targetLabel) : -1;
          if (existingIndex >= 0) {
            next[existingIndex] = material;
            return;
          }
          next.push(material);
        });
        return next;
      });
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setUploading(false);
    }
  }

  async function uploadTemplatePreview(files: FileList | null) {
    const file = files?.[0];
    if (!file) return;
    if (!fileMatchesMaterialKind(file, "image")) {
      setTemplateError("模板预览图只能上传图片。");
      return;
    }
    setTemplatePreviewUploading(true);
    setTemplateError(null);
    try {
      const [asset] = await uploadAssetFiles([file]);
      setTemplateForm((form) => ({
        ...form,
        previewImageUrl: asset.url,
        name: form.name || asset.name.replace(/\.[^.]+$/, "")
      }));
    } catch (err) {
      setTemplateError(err instanceof Error ? err.message : String(err));
    } finally {
      setTemplatePreviewUploading(false);
    }
  }

  function materialFromAsset(asset: Asset, existing: PromptMaterial[], preferredLabel?: string): PromptMaterial {
    const type = materialTypeForAsset(asset);
    const normalizedPreferredLabel = normalizeMaterialLabel(preferredLabel);
    return {
      id: asset.id,
      url: asset.url,
      type,
      role: "upload",
      label: normalizedPreferredLabel && materialTypeForLabel(normalizedPreferredLabel) === type
        ? normalizedPreferredLabel
        : nextMaterialLabel(type, existing),
      name: asset.name
    };
  }

  function removeMaterial(index: number) {
    setMaterials((items) => items.filter((_, itemIndex) => itemIndex !== index));
  }

  function insertToken(token?: string) {
    if (!token) return;
    insertAtCursor(token);
  }

  function insertAtCursor(text: string) {
    const textarea = promptRef.current;
    if (!textarea) {
      const joiner = prompt.endsWith("\n") || !prompt.trim() ? "" : "\n";
      setPrompt(`${prompt}${joiner}${text}`);
      return;
    }
    const start = textarea.selectionStart ?? prompt.length;
    const end = textarea.selectionEnd ?? prompt.length;
    const before = prompt.slice(0, start);
    const after = prompt.slice(end);
    const needsLeadingSpace = before.length > 0 && !/[\s，。；：、（(【\[]$/.test(before);
    const needsTrailingSpace = after.length > 0 && !/^[\s，。；：、）)】\]]/.test(after);
    const insertion = `${needsLeadingSpace ? " " : ""}${text}${needsTrailingSpace ? " " : ""}`;
    const nextPrompt = `${before}${insertion}${after}`;
    const nextCursor = before.length + insertion.length;
    setPrompt(nextPrompt);
    window.requestAnimationFrame(() => {
      textarea.focus();
      textarea.setSelectionRange(nextCursor, nextCursor);
    });
  }

  function applyImageTemplate(template: ImageTemplate) {
    if (activeTemplateId === template.id) {
      clearImageTemplateSelection();
      return;
    }
    if (!templateRestoreState) {
      setTemplateRestoreState({
        mode: generateMode,
        imagePrompt: promptByMode.image,
        imageSize,
        imageEngine,
        jimengInfluence,
        evolinkQuality
      });
    }
    applyTemplateToConsole(template, `已应用模板：${template.name}`);
  }

  function clearImageTemplateSelection() {
    const snapshot = templateRestoreState;
    setActiveTemplateId(null);
    setTemplateRestoreState(null);
    setMaterialDraft(null);
    setMentionState(null);
    setPromptScrollTop(0);
    setError(null);
    if (snapshot) {
      setMode(snapshot.mode);
      setPromptByMode((items) => ({ ...items, image: snapshot.imagePrompt }));
      setImageSize(snapshot.imageSize);
      setImageEngine(snapshot.imageEngine);
      setJimengInfluence(snapshot.jimengInfluence);
      setEvolinkQuality(snapshot.evolinkQuality);
    } else {
      setPromptByMode((items) => ({ ...items, image: "" }));
      setImageSize(imageSizePresets[0]);
      setImageEngine("jimeng");
      setJimengInfluence(jimengInfluenceOptions[1].id);
      setEvolinkQuality(evolinkQualityOptions[1].id);
    }
    setNotice("已取消模板选择");
  }

  function applyTemplateToConsole(template: ImageTemplate, message: string) {
    setMode("image");
    setPromptByMode((items) => ({ ...items, image: template.prompt }));
    setActiveTemplateId(template.id);
    setMaterialDraft(null);
    const templateEngine = normalizeImageEngine(template.settings.engine);
    if (templateEngine) setImageEngine(templateEngine);
    const preset = imageSizePresets.find((item) => item.width === template.settings.width && item.height === template.settings.height);
    if (preset) setImageSize(preset);
    const templateInfluence = jimengInfluenceOptions.find((option) => option.scale === template.settings.scale);
    if (templateInfluence) setJimengInfluence(templateInfluence.id);
    const templateQuality = evolinkQualityOptions.find((option) => option.quality === template.settings.quality);
    if (templateQuality) setEvolinkQuality(templateQuality.id);
    setMentionState(null);
    setPromptScrollTop(0);
    setNotice(message);
    setError(null);
  }

  function resetTemplateForm() {
    setTemplateForm(defaultTemplateFormForEngine(imageEngine));
    setEditingTemplateId(null);
    setTemplateError(null);
    setMaterialDraft(null);
    setTemplatePromptScrollTop(0);
  }

  function openTemplateCreator() {
    resetTemplateForm();
    setTemplateEditorOpen(true);
  }

  function openTemplateEditor(template: ImageTemplate) {
    setTemplateForm(formFromTemplate(template));
    setEditingTemplateId(template.id);
    setTemplateError(null);
    setMaterialDraft(null);
    setTemplatePromptScrollTop(0);
    setTemplateEditorOpen(true);
  }

  function closeTemplateEditor() {
    setTemplateEditorOpen(false);
    setEditingTemplateId(null);
    setTemplateError(null);
    setMaterialDraft(null);
    setTemplatePromptScrollTop(0);
  }

  async function saveTemplate() {
    const isEditing = Boolean(editingTemplateId);
    const endpoint = editingTemplateId ? `/api/image-templates/${encodeURIComponent(editingTemplateId)}` : "/api/image-templates";
    setTemplateSaving(true);
    setTemplateError(null);
    try {
      const response = await fetch(endpoint, {
        method: isEditing ? "PATCH" : "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(templatePayloadFromForm(templateForm))
      });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || "保存模板失败");
      const saved = payload.template as ImageTemplate;
      setImageTemplates((items) => {
        const rest = items.filter((item) => item.id !== saved.id);
        return [...rest, saved].sort(sortTemplates);
      });
      setTemplateForm(formFromTemplate(saved));
      setEditingTemplateId(null);
      setTemplateEditorOpen(false);
      applyTemplateToConsole(saved, isEditing ? "模板已更新并应用。" : "模板已保存并应用。");
    } catch (err) {
      setTemplateError(err instanceof Error ? err.message : String(err));
    } finally {
      setTemplateSaving(false);
    }
  }

  function insertTemplatePlaceholder(type: MaterialKind) {
    const placeholders = extractMaterialPlaceholders(templateForm.prompt).filter((placeholder) => placeholder.type === type);
    const nextIndex = placeholders.reduce((max, placeholder) => Math.max(max, placeholder.index), 0) + 1;
    const token = labelFor(type, nextIndex);
    const textarea = templatePromptRef.current;
    const value = templateForm.prompt;
    if (!textarea) {
      const joiner = value.endsWith("\n") || !value.trim() ? "" : " ";
      setTemplateForm((form) => ({ ...form, prompt: `${value}${joiner}${token} ` }));
      return;
    }
    const start = textarea.selectionStart ?? value.length;
    const end = textarea.selectionEnd ?? value.length;
    const before = value.slice(0, start);
    const after = value.slice(end);
    const needsLeadingSpace = before.length > 0 && !/[\s，。；：、（(【\[]$/.test(before);
    const insertion = `${needsLeadingSpace ? " " : ""}${token} `;
    const nextPrompt = `${before}${insertion}${after}`;
    const nextCursor = before.length + insertion.length;
    setTemplateForm((form) => ({ ...form, prompt: nextPrompt }));
    window.requestAnimationFrame(() => {
      textarea.focus();
      textarea.setSelectionRange(nextCursor, nextCursor);
    });
  }

  function startMaterialDraft(target: MaterialDraftTarget, index: number) {
    setMentionState(null);
    setMaterialDraft({ target, index, query: "", activeIndex: null });
  }

  function updateMaterialDraftQuery(value: string) {
    setMaterialDraft((draft) => draft ? { ...draft, query: value.replace(/^@+/, ""), activeIndex: null } : draft);
  }

  function confirmMaterialDraft(option?: MaterialDraftOption) {
    if (!materialDraft) return;
    const sourcePrompt = materialDraft.target === "template" ? templateForm.prompt : prompt;
    const token = option?.token || normalizeMaterialDraftToken(materialDraft.query, sourcePrompt);
    if (!token) return;
    const next = insertMaterialDraftToken(sourcePrompt, materialDraft.index, token);
    const target = materialDraft.target;
    setMaterialDraft(null);
    setMentionState(null);
    if (target === "template") {
      setTemplateForm((form) => ({ ...form, prompt: next.text }));
      window.requestAnimationFrame(() => {
        templatePromptRef.current?.focus();
        templatePromptRef.current?.setSelectionRange(next.cursor, next.cursor);
      });
      return;
    }
    setPrompt(next.text);
    window.requestAnimationFrame(() => {
      promptRef.current?.focus();
      promptRef.current?.setSelectionRange(next.cursor, next.cursor);
    });
  }

  function handleMaterialDraftKeyDown(event: KeyboardEvent<HTMLInputElement>) {
    if (!materialDraft) return;
    if (event.key === "Escape") {
      event.preventDefault();
      const target = materialDraft.target;
      setMaterialDraft(null);
      window.requestAnimationFrame(() => {
        (target === "template" ? templatePromptRef.current : promptRef.current)?.focus();
      });
      return;
    }
    if (event.key === "ArrowDown" && materialDraftOptions.length) {
      event.preventDefault();
      setMaterialDraft((draft) => draft ? { ...draft, activeIndex: draft.activeIndex === null ? 0 : (draft.activeIndex + 1) % materialDraftOptions.length } : draft);
      return;
    }
    if (event.key === "ArrowUp" && materialDraftOptions.length) {
      event.preventDefault();
      setMaterialDraft((draft) => draft ? { ...draft, activeIndex: draft.activeIndex === null ? materialDraftOptions.length - 1 : (draft.activeIndex - 1 + materialDraftOptions.length) % materialDraftOptions.length } : draft);
      return;
    }
    if (event.key === "Enter" || event.key === "Tab") {
      event.preventDefault();
      confirmMaterialDraft(materialDraftOptions[materialDraft.activeIndex ?? 0]);
    }
  }

  function handleTemplatePromptInput(value: string, cursor: number) {
    const draft = detectMaterialDraftStart(templateForm.prompt, value, cursor);
    if (draft) {
      setTemplateForm((form) => ({ ...form, prompt: draft.text }));
      startMaterialDraft("template", draft.index);
      return;
    }
    setTemplateForm((form) => ({ ...form, prompt: value }));
  }

  function selectMention(material: PromptMaterial) {
    if (!material.label) return;
    const textarea = promptRef.current;
    const end = textarea?.selectionStart ?? prompt.length;
    const start = mentionState?.start ?? end;
    const before = prompt.slice(0, start);
    const after = prompt.slice(end);
    const needsTrailingSpace = after.length > 0 && !/^[\s，。；：、）)】\]]/.test(after);
    const insertion = `${material.label}${needsTrailingSpace ? " " : ""}`;
    const nextPrompt = `${before}${insertion}${after}`;
    const nextCursor = before.length + insertion.length;
    setPrompt(nextPrompt);
    setMentionState(null);
    window.requestAnimationFrame(() => {
      textarea?.focus();
      textarea?.setSelectionRange(nextCursor, nextCursor);
    });
  }

  function handlePromptKeyDown(event: KeyboardEvent<HTMLTextAreaElement>) {
    if (!mentionState) return;
    if (event.key === "Escape") {
      setMentionState(null);
      return;
    }
    if (!mentionSuggestions.length) return;
    if (event.key === "ArrowDown") {
      event.preventDefault();
      setActiveMentionIndex((index) => index === null ? 0 : (index + 1) % mentionSuggestions.length);
    }
    if (event.key === "ArrowUp") {
      event.preventDefault();
      setActiveMentionIndex((index) => index === null ? mentionSuggestions.length - 1 : (index - 1 + mentionSuggestions.length) % mentionSuggestions.length);
    }
    if (event.key === "Enter" || event.key === "Tab") {
      event.preventDefault();
      selectMention(mentionSuggestions[activeMentionIndex ?? 0] || mentionSuggestions[0]);
    }
  }

  async function submit() {
    if (isImageEditMode) return;
    if (missingMaterialPlaceholders.length) {
      setError(`请先上传 ${missingMaterialPlaceholders.map((placeholder) => placeholder.token).join("、")}。`);
      return;
    }
    setBusy(true);
    setError(null);
    setNotice(null);
    try {
      const endpoint = generateMode === "image" ? "/api/generations/image" : "/api/generations/video";
      const body = generateMode === "image"
        ? {
            capability: "image.generate",
            engine: imageEngine,
            prompt,
            materials: materials.filter((material) => material.type === "image"),
            width: imageSize.width,
            height: imageSize.height,
            ...(imageEngine === "evolink"
              ? { quality: selectedEvolinkQuality.quality }
              : { scale: selectedJimengInfluence.scale }),
            force_single: true
          }
        : {
            prompt,
            materials,
            settings: {
              ratio: videoRatio,
              duration: videoDuration,
              resolution: videoResolution
            }
          };
      const response = await fetch(endpoint, {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify(body)
      });
      const payload = await response.json();
      if (!response.ok) throw new Error(payload.error || "提交生成失败");
      const submittedJob = payload.job as GenerationJob | undefined;
      if (submittedJob?.id) {
        setRecentJobs((items) => [submittedJob, ...items.filter((item) => item.id !== submittedJob.id)].sort((a, b) => b.createdAt.localeCompare(a.createdAt)));
      }
      void loadTaskModuleData(() => true, { silent: true });
      setNotice(`${generateMode === "image" ? "图片" : "视频"}生成已提交。任务已进入「结果」，生成完成后结果资产会自动保留。`);
      setPrompt("");
      setActiveTemplateId(null);
      setMaterials([]);
      setMaterialPage(1);
      setMentionState(null);
      setPromptScrollTop(0);
    } catch (err) {
      setError(err instanceof Error ? err.message : String(err));
    } finally {
      setBusy(false);
    }
  }

  function renderCreateModeBar() {
    return (
      <div className="create-mode-bar">
        <div className="segmented mode-switch" aria-label="创作类型">
          <button type="button" className={clsx(mode === "image" && "active")} aria-pressed={mode === "image"} onClick={() => setMode("image")}>
            <ImagePlus size={17} />
            图片
          </button>
          <button type="button" className={clsx(mode === "video" && "active")} aria-pressed={mode === "video"} onClick={() => setMode("video")}>
            <Film size={17} />
            视频
          </button>
          <button type="button" className={clsx(mode === "inpaint" && "active")} aria-pressed={mode === "inpaint"} onClick={() => setMode("inpaint")}>
            <Paintbrush size={17} />
            局部重绘
          </button>
          <button type="button" className={clsx(mode === "upscale" && "active")} aria-pressed={mode === "upscale"} onClick={() => setMode("upscale")}>
            <ImageUp size={17} />
            智能超清
          </button>
        </div>
        {!isImageEditMode ? (
          <div className="toolbar create-actions">
            <button
              className="button primary create-submit-button"
              type="button"
              disabled={submitDisabled}
              onClick={submit}
              aria-label={generateMode === "image" ? "生成图片" : "生成视频"}
              title={submitTitle}
            >
              {busy ? <Loader2 className="spin" size={18} /> : <Send size={18} />}
              <span className="create-submit-label">{generateMode === "image" ? "生成图片" : "生成视频"}</span>
            </button>
          </div>
        ) : null}
      </div>
    );
  }

  return (
    <div className={clsx("create-studio", isImageEditMode && "enhance-studio")} ref={studioRef}>
      {isImageEditMode ? (
        <div className="create-workbench-layout" ref={(node) => { modePanelRef.current = node; }} data-animate>
          <section className="panel create-workbench-panel">
            {renderCreateModeBar()}
            <ImageEditor initialMode={mode} hideModeSwitch />
          </section>
        </div>
      ) : (
        <div className="create-workbench-layout with-template-column" ref={(node) => { modePanelRef.current = node; }} data-animate>
          <aside className="panel image-template-column" aria-label="模板选择">
            <div className="image-template-rail-head">
              <h2>模板选择</h2>
              {templatesLoading ? <Loader2 className="spin" size={16} /> : null}
            </div>
            <button className="button template-add-button" type="button" onClick={openTemplateCreator} disabled={templateSaving}>
              <Plus size={16} />
              添加模板
            </button>
            {templateError && !templateEditorOpen ? <div className="callout compact-callout" role="alert">{templateError}</div> : null}
            <div className="image-template-rail-list">
              {imageTemplates.length ? (
                imageTemplates.map((template) => (
                  <article
                    className={clsx("image-template-rail-card", activeTemplateId === template.id && "active")}
                    key={template.id}
                    title={template.name}
                  >
                    <button
                      className="image-template-preview-button"
                      type="button"
                      title="放大预览"
                      aria-label={`放大预览 ${template.name}`}
                      onClick={() => setPreviewTemplate(template)}
                    >
                      {renderImageTemplatePreview(template)}
                    </button>
                    <div className="image-template-card-footer">
                      <div className="image-template-card-copy">
                        <strong>{template.name}</strong>
                      </div>
                      <button
                        className="image-template-edit-button"
                        type="button"
                        title="编辑模板"
                        aria-label={`编辑模板 ${template.name}`}
                        onClick={() => openTemplateEditor(template)}
                      >
                        <Pencil size={14} />
                      </button>
                      <button
                        className="image-template-select-button"
                        type="button"
                        onClick={() => applyImageTemplate(template)}
                        title={`选择模板 ${template.name}`}
                        aria-pressed={activeTemplateId === template.id}
                      >
                        {activeTemplateId === template.id ? <Check size={14} /> : <Plus size={14} />}
                        {activeTemplateId === template.id ? "取消选择" : "选择模板"}
                      </button>
                    </div>
                  </article>
                ))
              ) : !templatesLoading && !templateError ? (
                <div className="template-empty compact">暂无模板</div>
              ) : null}
            </div>
          </aside>
          <section className="panel create-workbench-panel create-center-panel">
            <div className="create-center-head">
              <h2>制作中心</h2>
              {renderCreateModeBar()}
            </div>
            <div className="create-workbench-grid">
              <div className="create-main-column">

        {error || notice ? (
          <div className="studio-messages top-studio-messages" ref={feedbackRef}>
            {error ? <div className="callout" role="alert">{error}</div> : null}
            {notice ? <div className="callout success-callout" role="status" aria-live="polite">{notice}</div> : null}
          </div>
        ) : null}

        <div className="field prompt-field">
          <div className="prompt-label-row">
            <label htmlFor="createPrompt">{generateMode === "image" ? "图片提示词" : "视频提示词"}</label>
            <label className="icon-button prompt-upload-button" title="上传素材" aria-label="上传素材">
              {uploading ? <Loader2 className="spin" size={18} /> : <Upload size={18} />}
              <input
                type="file"
                multiple
                accept={materialUploadAccept}
                onChange={(event) => {
                  void uploadFiles(event.target.files);
                  event.currentTarget.value = "";
                }}
              />
            </label>
          </div>
          <div className="prompt-editor-wrap">
            <div className="prompt-token-layer" aria-hidden="true">
              <div style={{ transform: `translateY(-${promptScrollTop}px)` }}>
                {renderPromptTokenLayer(prompt, materials)}
              </div>
            </div>
            <textarea
              id="createPrompt"
              ref={promptRef}
              value={prompt}
              onChange={(event) => {
                handlePromptInput(event.target.value, event.target.selectionStart);
              }}
              onInput={(event) => handlePromptInput(event.currentTarget.value, event.currentTarget.selectionStart)}
              onClick={(event) => updateMention(event.currentTarget.value, event.currentTarget.selectionStart)}
              onKeyDown={handlePromptKeyDown}
              onKeyUp={(event) => updateMention(event.currentTarget.value, event.currentTarget.selectionStart)}
              onScroll={(event) => setPromptScrollTop(event.currentTarget.scrollTop)}
              placeholder="输入营销内容提示词，键入 @ 选择已上传素材"
            />
            {materialDraft?.target === "prompt" ? renderMaterialDraftPanel(
              materialDraft,
              materialDraftOptions,
              materialDraftInputRef,
              updateMaterialDraftQuery,
              handleMaterialDraftKeyDown,
              confirmMaterialDraft
            ) : null}
            {!materialDraft && mentionState && mentionSuggestions.length ? (
              <div className="mention-popover">
                {mentionSuggestions.map((material, index) => (
                  <button
                    className={clsx("mention-option", index === activeMentionIndex && "active")}
                    type="button"
                    key={`${material.label}-${material.url}`}
                    onMouseDown={(event) => {
                      event.preventDefault();
                      selectMention(material);
                    }}
                  >
                    {renderMaterialPreview(material, "tiny")}
                    <span className="chip-token">{material.label}</span>
                    <span className="chip-name">{material.name || material.url}</span>
                    <span className="mention-kind">{shortTypeName(material.type)}</span>
                  </button>
                ))}
              </div>
            ) : null}
          </div>

          {missingMaterialPlaceholders.length ? (
            <div className="material-placeholder-board" aria-label="待上传模板素材">
              {missingMaterialPlaceholders.map((placeholder) => (
                <label className={clsx("material-placeholder-card", placeholder.type, uploading && "uploading")} key={placeholder.token} title={`上传${placeholder.token}`}>
                  <span className="material-placeholder-icon" aria-hidden="true">
                    {uploading ? <Loader2 className="spin" size={18} /> : <Upload size={18} />}
                  </span>
                  <span className="material-placeholder-copy">
                    <span className="chip-token">{placeholder.token}</span>
                    <span className="material-placeholder-state">待上传{shortTypeName(placeholder.type)}</span>
                  </span>
                  <input
                    type="file"
                    accept={acceptForMaterialKind(placeholder.type)}
                    disabled={uploading}
                    onChange={(event) => {
                      void uploadFiles(event.target.files, { targetLabel: placeholder.token, expectedType: placeholder.type });
                      event.currentTarget.value = "";
                    }}
                  />
                </label>
              ))}
            </div>
          ) : null}

          {materials.length ? (
            <>
              <div className="material-board prompt-material-board" aria-label="已上传素材" ref={materialBoardRef}>
                {visibleMaterials.map((material, index) => (
                  <article
                    className={clsx("material-card", material.type, materialIsReferenced(material, referencedMaterialLabels) ? "referenced" : "missing")}
                    key={`${material.url}-${materialPageOffset + index}`}
                  >
                    <button
                      className="material-card-main"
                      type="button"
                      title={`${material.label || ""} ${material.name || material.url}`}
                      onClick={() => insertToken(material.label)}
                    >
                      {renderMaterialPreview(material, "large")}
                      <span className="material-card-copy">
                        <span className="material-card-head">
                          <span className="chip-token">{material.label}</span>
                          <span className="material-state">{materialIsReferenced(material, referencedMaterialLabels) ? "已引用" : "未引用"}</span>
                        </span>
                        <span className="chip-name">{material.name || material.url}</span>
                      </span>
                    </button>
                    <button
                      className="material-remove"
                      type="button"
                      title="移除素材"
                      onClick={(event) => {
                        event.stopPropagation();
                        removeMaterial(materialPageOffset + index);
                      }}
                    >
                      <X size={14} aria-hidden="true" />
                    </button>
                  </article>
                ))}
              </div>
              <Pagination page={materialPage} pageSize={MATERIAL_PAGE_SIZE} total={materials.length} label="素材分页" onPageChange={setMaterialPage} />
            </>
          ) : null}
        </div>

        <div className="inline-settings">
          {generateMode === "image" ? (
            <>
              <div className="field inline-field">
                <label htmlFor="imageEngine">生图引擎</label>
                <select id="imageEngine" value={imageEngine} onChange={(event) => setImageEngine(normalizeImageEngine(event.target.value) || "jimeng")}>
                  <option value="jimeng">即梦</option>
                  <option value="evolink">Image2</option>
                </select>
              </div>
              <div className="field inline-field">
                <label htmlFor="imageSize">画幅</label>
                <select
                  id="imageSize"
                  value={imageSize.label}
                  onChange={(event) => setImageSize(imageSizePresets.find((item) => item.label === event.target.value) || imageSizePresets[0])}
                >
                  {imageSizePresets.map((preset) => <option key={preset.label}>{preset.label}</option>)}
                </select>
              </div>
              <div className="field inline-field">
                <label htmlFor="imageEngineTuning">{imageEngine === "evolink" ? "生成质量" : "文本影响"}</label>
                {imageEngine === "evolink" ? (
                  <select id="imageEngineTuning" value={evolinkQuality} onChange={(event) => setEvolinkQuality(event.target.value)}>
                    {evolinkQualityOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
                  </select>
                ) : (
                  <select id="imageEngineTuning" value={jimengInfluence} onChange={(event) => setJimengInfluence(event.target.value)}>
                    {jimengInfluenceOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
                  </select>
                )}
              </div>
            </>
          ) : (
            <>
              <div className="field inline-field">
                <label htmlFor="videoRatio">比例</label>
                <select id="videoRatio" value={videoRatio} onChange={(event) => setVideoRatio(event.target.value)}>
                  {VIDEO_RATIOS.map((ratio) => (
                    <option key={ratio} value={ratio}>{ratio === "adaptive" ? "自适应" : ratio}</option>
                  ))}
                </select>
              </div>
              <div className="field inline-field">
                <label htmlFor="videoDuration">时长</label>
                <select
                  id="videoDuration"
                  value={videoDuration}
                  onChange={(event) => setVideoDuration(clampVideoDuration(event.target.value, videoDuration, { allowAuto: false }))}
                >
                  {VIDEO_DURATION_OPTIONS.map((seconds) => <option key={seconds} value={seconds}>{seconds} 秒</option>)}
                </select>
              </div>
              <div className="field inline-field">
                <label htmlFor="videoResolution">清晰度</label>
                <select id="videoResolution" value={videoResolution} onChange={(event) => setVideoResolution(event.target.value)}>
                  {VIDEO_RESOLUTIONS.map((resolution) => <option key={resolution} value={resolution}>{resolution}</option>)}
                </select>
              </div>
            </>
          )}
            </div>
          </div>
          {templateEditorOpen ? (
            <div className="template-editor-backdrop" role="dialog" aria-modal="true" aria-label={editingTemplateId ? "编辑模板" : "添加模板"}>
              <div className="template-editor-dialog">
                <div className="template-editor-head">
                  <div className="template-editor-title">
                    <span className="template-editor-mark" aria-hidden="true">
                      <ImagePlus size={18} />
                    </span>
                    <h2>{editingTemplateId ? "编辑模板" : "添加模板"}</h2>
                  </div>
                  <button
                    className="icon-button"
                    type="button"
                    title="关闭"
                    aria-label="关闭"
                    onClick={closeTemplateEditor}
                    disabled={templateSaving}
                  >
                    <X size={16} />
                  </button>
                </div>
                {templateError ? <div className="callout compact-callout" role="alert">{templateError}</div> : null}
                <div className="template-editor-body">
                  <aside className="template-editor-preview-pane">
                    <div className="template-preview-pane-head">
                      <span>效果预览图</span>
                      <span className={clsx("template-required-pill", templateForm.previewImageUrl && "complete")}>
                        {templateForm.previewImageUrl ? "已上传" : "必填"}
                      </span>
                    </div>
                    <label className={clsx("template-preview-uploader", templateForm.previewImageUrl && "has-image", templatePreviewUploading && "uploading")}>
                      <span className="template-preview-uploader-media" aria-hidden="true">
                        {templatePreviewUploading ? <Loader2 className="spin" size={22} /> : templateForm.previewImageUrl ? <img src={templateForm.previewImageUrl} alt="" /> : <Upload size={22} />}
                      </span>
                      <span>{templateForm.previewImageUrl ? "重新上传" : "点击上传"}</span>
                      <input
                        type="file"
                        accept={templatePreviewAccept}
                        disabled={templatePreviewUploading}
                        onChange={(event) => {
                          void uploadTemplatePreview(event.target.files);
                          event.currentTarget.value = "";
                        }}
                      />
                    </label>
                    <div className="template-preview-meta" aria-label="模板参数预览">
                      <span>{imageEngineLabel(templateForm.engine)}</span>
                      <span>{templateForm.engine === "evolink" ? selectedTemplateEvolinkQuality.label : selectedTemplateJimengInfluence.label}</span>
                      <span>{templateForm.size}</span>
                    </div>
                  </aside>
                  <div className="template-inline-form template-editor-form">
                    <label className="field">
                      <span>模板名称</span>
                      <input value={templateForm.name} onChange={(event) => setTemplateForm((form) => ({ ...form, name: event.target.value }))} />
                    </label>
                    <div className="field template-engine-field">
                      <span>生图引擎</span>
                      <div className="segmented template-engine-choice" aria-label="生图引擎">
                        <button
                          type="button"
                          className={clsx(templateForm.engine === "jimeng" && "active")}
                          aria-pressed={templateForm.engine === "jimeng"}
                          onClick={() => setTemplateForm((form) => ({ ...form, engine: "jimeng" }))}
                        >
                          即梦
                        </button>
                        <button
                          type="button"
                          className={clsx(templateForm.engine === "evolink" && "active")}
                          aria-pressed={templateForm.engine === "evolink"}
                          onClick={() => setTemplateForm((form) => ({ ...form, engine: "evolink" }))}
                        >
                          Image2
                        </button>
                      </div>
                    </div>
                    <label className="field">
                      <span>{templateForm.engine === "evolink" ? "生成质量" : "文本影响"}</span>
                      {templateForm.engine === "evolink" ? (
                        <select value={templateForm.evolinkQuality} onChange={(event) => setTemplateForm((form) => ({ ...form, evolinkQuality: event.target.value }))}>
                          {evolinkQualityOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
                        </select>
                      ) : (
                        <select value={templateForm.jimengInfluence} onChange={(event) => setTemplateForm((form) => ({ ...form, jimengInfluence: event.target.value }))}>
                          {jimengInfluenceOptions.map((option) => <option key={option.id} value={option.id}>{option.label}</option>)}
                        </select>
                      )}
                    </label>
                    <label className="field">
                      <span>画幅</span>
                      <select value={templateForm.size} onChange={(event) => setTemplateForm((form) => ({ ...form, size: event.target.value }))}>
                        {imageSizePresets.map((preset) => <option value={preset.label} key={preset.label}>{preset.label}</option>)}
                      </select>
                    </label>
                    <label className="field">
                      <span>排序</span>
                      <input type="number" value={templateForm.sortOrder} onChange={(event) => setTemplateForm((form) => ({ ...form, sortOrder: event.target.value }))} />
                    </label>
                    <label className="field template-prompt-field">
                      <span>预设提示词</span>
                      <div className="template-placeholder-tools" aria-label="插入占位符">
                        <button type="button" onClick={() => insertTemplatePlaceholder("image")}>
                          <Plus size={13} />
                          图片
                        </button>
                        <button type="button" onClick={() => insertTemplatePlaceholder("video")}>
                          <Plus size={13} />
                          视频
                        </button>
                        <button type="button" onClick={() => insertTemplatePlaceholder("audio")}>
                          <Plus size={13} />
                          音频
                        </button>
                      </div>
                      <div className="prompt-editor-wrap template-prompt-editor-wrap">
                        <div className="prompt-token-layer" aria-hidden="true">
                          <div style={{ transform: `translateY(-${templatePromptScrollTop}px)` }}>
                            {renderPromptTokenLayer(templateForm.prompt, [])}
                          </div>
                        </div>
                        <textarea
                          ref={templatePromptRef}
                          value={templateForm.prompt}
                          onChange={(event) => handleTemplatePromptInput(event.target.value, event.target.selectionStart)}
                          onInput={(event) => handleTemplatePromptInput(event.currentTarget.value, event.currentTarget.selectionStart)}
                          onScroll={(event) => setTemplatePromptScrollTop(event.currentTarget.scrollTop)}
                          rows={7}
                          placeholder="例如：以 @图片1 为主体，生成干净高级的商品主视觉"
                        />
                        {materialDraft?.target === "template" ? renderMaterialDraftPanel(
                          materialDraft,
                          materialDraftOptions,
                          materialDraftInputRef,
                          updateMaterialDraftQuery,
                          handleMaterialDraftKeyDown,
                          confirmMaterialDraft
                        ) : null}
                      </div>
                    </label>
                    {templateFormPlaceholders.length ? (
                      <div className="template-placeholder-strip" aria-label="模板占位符">
                        {templateFormPlaceholders.map((placeholder) => (
                          <span className="template-placeholder-chip" key={placeholder.token}>
                            {placeholder.token}
                          </span>
                        ))}
                      </div>
                    ) : null}
                  </div>
                </div>
                <div className="template-editor-actions">
                  <button className="button" type="button" onClick={closeTemplateEditor} disabled={templateSaving}>
                    取消
                  </button>
                  <button className="button primary" type="button" onClick={() => void saveTemplate()} disabled={templateSaving || templatePreviewUploading || !templateFormReady}>
                    {templateSaving ? <Loader2 className="spin" size={18} /> : <Save size={18} />}
                    {editingTemplateId ? "保存修改" : "保存模板"}
                  </button>
                </div>
              </div>
            </div>
          ) : null}
          {previewTemplate ? (
            <div className="template-preview-backdrop" role="dialog" aria-modal="true" aria-label="模板预览">
              <div className="template-preview-dialog">
                <div className="template-preview-dialog-head">
                  <h2>{previewTemplate.name}</h2>
                  <button className="icon-button" type="button" title="关闭" aria-label="关闭" onClick={() => setPreviewTemplate(null)}>
                    <X size={16} />
                  </button>
                </div>
                <div className="template-preview-dialog-media">
                  {previewTemplate.previewImageUrl ? <img src={previewTemplate.previewImageUrl} alt="" /> : <ImagePlus size={40} />}
                </div>
                <div className="template-preview-dialog-actions">
                  {previewTemplate.previewImageUrl ? (
                    <a className="button" href={previewTemplate.previewImageUrl} download>
                      <Download size={16} />
                      下载图片
                    </a>
                  ) : null}
                  <button className="button" type="button" onClick={() => {
                    const template = previewTemplate;
                    setPreviewTemplate(null);
                    openTemplateEditor(template);
                  }}>
                    <Pencil size={16} />
                    编辑模板
                  </button>
                  <button className="button primary" type="button" onClick={() => {
                    applyImageTemplate(previewTemplate);
                    setPreviewTemplate(null);
                  }}>
                    选择模板
                  </button>
                </div>
              </div>
            </div>
          ) : null}
          {previewTaskAsset ? (
            <div className="asset-preview-backdrop" role="presentation" onMouseDown={() => setPreviewTaskAsset(null)}>
              <div className="asset-preview-dialog task-preview-dialog" role="dialog" aria-modal="true" aria-label={`预览 ${previewTaskAsset.name}`} onMouseDown={(event) => event.stopPropagation()}>
                <div className="asset-preview-head">
                  <div>
                    <strong>{previewTaskAsset.name}</strong>
                    <span className="muted compact-hint">任务生成资产</span>
                  </div>
                  <div className="asset-preview-actions">
                    <a className="icon-button" href={assetDownloadUrl(previewTaskAsset)} title="下载" aria-label={`下载 ${previewTaskAsset.name}`}>
                      <Download size={18} />
                    </a>
                    <button className="icon-button" type="button" title="关闭预览" aria-label="关闭预览" onClick={() => setPreviewTaskAsset(null)}>
                      <X size={18} />
                    </button>
                  </div>
                </div>
                <div className="asset-preview-media">
                  {renderTaskAssetPreviewLarge(previewTaskAsset)}
                </div>
              </div>
            </div>
          ) : null}
            </div>
          </section>
          <aside className="panel create-task-column" aria-label="任务模块">
            <div className="create-task-head">
              <div>
                <h2>任务模块</h2>
                <span>{recentJobs.length ? `${recentJobs.length} 个任务` : "任务列表"}</span>
              </div>
              <button
                className="icon-button"
                type="button"
                title="刷新任务"
                aria-label="刷新任务"
                onClick={() => void loadTaskModuleData()}
                disabled={tasksLoading}
              >
                {tasksLoading ? <Loader2 className="spin" size={16} /> : <RefreshCw size={16} />}
              </button>
            </div>
            {tasksError ? <div className="callout compact-callout" role="alert">{tasksError}</div> : null}
            <div className="create-task-list">
              {recentJobs.map((job) => (
                <article className="create-task-card" key={job.id}>
                  {renderTaskThumbnail(job, taskAssetById, setPreviewTaskAsset)}
                  <div className="create-task-body">
                    <h3 title={taskName(job)}>{taskName(job)}</h3>
                    <div className="create-task-meta">
                      <span className={`status ${job.status}`}>{statusLabel(job.status)}</span>
                      <span>{durationLabel(job, durationNow)}</span>
                    </div>
                  </div>
                  <a className="button mini-button create-task-link" href={taskDetailHref(job)}>
                    <ExternalLink size={14} />
                    查看详情
                  </a>
                </article>
              ))}
              {!recentJobs.length && !tasksLoading && !tasksError ? (
                <div className="template-empty compact">暂无任务</div>
              ) : null}
            </div>
          </aside>
        </div>
      )}
    </div>
  );
}

function renderTaskThumbnail(job: GenerationJob, assetById: Map<string, Asset>, onPreview: (asset: Asset) => void) {
  const asset = job.outputAssetIds.map((assetId) => assetById.get(assetId)).find((item): item is Asset => Boolean(item));
  if (!asset) {
    return (
      <div className={clsx("create-task-thumb create-task-thumb-placeholder", isPendingTask(job) && "generating")} aria-hidden="true">
        {isPendingTask(job) ? <Loader2 className="spin" size={18} /> : <ImageIcon size={18} />}
        <span>{isPendingTask(job) ? "生成中" : "无结果"}</span>
      </div>
    );
  }
  const isVideo = asset.kind === "video" || materialTypeForAsset(asset) === "video";
  const isPreviewable = !isVideo;
  const media = isVideo
    ? <video src={asset.url} muted playsInline preload="metadata" />
    : <img src={asset.url} alt="" />;
  if (isPreviewable) {
    return (
      <button className="create-task-thumb create-task-thumb-button" type="button" onClick={() => onPreview(asset)} aria-label={`查看大图 ${asset.name}`}>
        {media}
      </button>
    );
  }
  return <div className="create-task-thumb" aria-hidden="true">{media}</div>;
}

function renderTaskAssetPreviewLarge(asset: Asset) {
  if (asset.kind === "video" || materialTypeForAsset(asset) === "video") {
    return <video src={asset.url} controls playsInline />;
  }
  return <img src={asset.url} alt={asset.name} />;
}

function assetDownloadUrl(asset: Asset) {
  return `/api/assets/${encodeURIComponent(asset.id)}/download`;
}

function isPendingTask(job: GenerationJob) {
  return job.status === "queued" || job.status === "running";
}

function taskName(job: GenerationJob) {
  const text = job.prompt?.trim();
  return text ? text : capabilityLabel(job.capability);
}

function taskDetailHref(job: GenerationJob) {
  return `/assets?view=tasks&taskId=${encodeURIComponent(job.id)}`;
}

function statusLabel(status: GenerationJob["status"]) {
  if (status === "queued") return "排队中";
  if (status === "running") return "生成中";
  if (status === "succeeded") return "已完成";
  if (status === "failed") return "失败";
  if (status === "expired") return "已过期";
  if (status === "cancelled") return "已取消";
  return status;
}

function isTerminalStatus(status: GenerationJob["status"]) {
  return status === "succeeded" || status === "failed" || status === "expired" || status === "cancelled";
}

function capabilityLabel(capability?: GenerationJob["capability"]) {
  if (capability === "image.generate") return "图片生成";
  if (capability === "image.inpaint") return "局部重绘";
  if (capability === "image.upscale") return "智能超清";
  if (capability === "video.generate") return "视频生成";
  return "生成任务";
}

function durationLabel(job: GenerationJob, now: number) {
  const terminal = isTerminalStatus(job.status);
  return `${terminal ? "耗时" : "已耗时"} ${durationValue(job, now)}`;
}

function durationValue(job: GenerationJob, now: number) {
  const start = new Date(job.createdAt).getTime();
  const terminal = isTerminalStatus(job.status);
  const end = terminal ? new Date(job.updatedAt).getTime() : now;
  if (!Number.isFinite(start) || !Number.isFinite(end) || end < start) return "--";
  return formatDuration(end - start);
}

function formatDuration(ms: number) {
  const totalSeconds = Math.max(0, Math.round(ms / 1000));
  const hours = Math.floor(totalSeconds / 3600);
  const minutes = Math.floor((totalSeconds % 3600) / 60);
  const seconds = totalSeconds % 60;
  if (hours) return `${hours}小时${minutes}分`;
  if (minutes) return `${minutes}分${seconds}秒`;
  return `${seconds}秒`;
}

function renderMaterialPreview(material: PromptMaterial, size: "normal" | "tiny" | "large" = "normal") {
  return (
    <span className={clsx("material-preview", size, material.type)} aria-hidden="true">
      {material.type === "image" ? <img src={material.url} alt="" /> : null}
      {material.type === "video" ? <video src={material.url} muted playsInline preload="metadata" /> : null}
      {material.type === "audio" ? <Music size={size === "tiny" ? 13 : size === "large" ? 20 : 15} /> : null}
    </span>
  );
}

function formFromTemplate(template: ImageTemplate): TemplateForm {
  const engine = normalizeImageEngine(template.settings.engine) || "jimeng";
  const jimengInfluence = jimengInfluenceOptions.find((option) => option.scale === template.settings.scale)?.id || jimengInfluenceOptions[1].id;
  const evolinkQuality = evolinkQualityOptions.find((option) => option.quality === template.settings.quality)?.id || evolinkQualityOptions[1].id;
  return {
    name: template.name,
    prompt: template.prompt,
    previewImageUrl: template.previewImageUrl || "",
    engine,
    jimengInfluence,
    evolinkQuality,
    size: sizeLabelFromSettings(template.settings.width, template.settings.height),
    sortOrder: String(template.sortOrder ?? 0)
  };
}

function templatePayloadFromForm(form: TemplateForm) {
  const size = imageSizePresets.find((preset) => preset.label === form.size) || imageSizePresets[0];
  const selectedInfluence = jimengInfluenceOptions.find((option) => option.id === form.jimengInfluence) || jimengInfluenceOptions[1];
  const selectedQuality = evolinkQualityOptions.find((option) => option.id === form.evolinkQuality) || evolinkQualityOptions[1];
  return {
    name: form.name,
    prompt: form.prompt,
    previewImageUrl: form.previewImageUrl,
    settings: {
      engine: form.engine,
      width: size.width,
      height: size.height,
      forceSingle: true,
      ...(form.engine === "evolink"
        ? { quality: selectedQuality.quality }
        : { scale: selectedInfluence.scale })
    },
    sortOrder: Number(form.sortOrder || 0)
  };
}

function defaultTemplateFormForEngine(engine: ImageGenerateEngine): TemplateForm {
  return {
    ...defaultTemplateForm,
    engine,
    jimengInfluence: jimengInfluenceOptions[1].id,
    evolinkQuality: evolinkQualityOptions[1].id
  };
}

function sizeLabelFromSettings(width?: number, height?: number) {
  return imageSizePresets.find((preset) => preset.width === width && preset.height === height)?.label || imageSizePresets[0].label;
}

function normalizeImageEngine(value?: string): ImageGenerateEngine | undefined {
  if (value === "jimeng" || value === "evolink") return value;
  return undefined;
}

function imageEngineLabel(engine: ImageGenerateEngine) {
  return engine === "evolink" ? "Image2" : "即梦";
}

function sortTemplates(a: ImageTemplate, b: ImageTemplate) {
  const order = (a.sortOrder || 0) - (b.sortOrder || 0);
  if (order !== 0) return order;
  return (b.updatedAt || "").localeCompare(a.updatedAt || "");
}

function renderImageTemplatePreview(template: ImageTemplate) {
  return (
    <span className="image-template-preview" aria-hidden="true">
      {template.previewImageUrl ? <img src={template.previewImageUrl} alt="" /> : <ImagePlus size={22} />}
    </span>
  );
}

function renderMaterialDraftPanel(
  draft: MaterialDraftState,
  options: MaterialDraftOption[],
  inputRef: RefObject<HTMLInputElement | null>,
  onQueryChange: (value: string) => void,
  onKeyDown: (event: KeyboardEvent<HTMLInputElement>) => void,
  onConfirm: (option?: MaterialDraftOption) => void
) {
  return (
    <div className="material-draft-panel" role="group" aria-label="素材占位输入">
      <div className="material-draft-entry">
        <span className="material-draft-prefix" aria-hidden="true">@</span>
        <input
          ref={inputRef}
          value={draft.query}
          onChange={(event) => onQueryChange(event.target.value)}
          onKeyDown={onKeyDown}
          placeholder="图片1 / 视频1 / 音频1"
          aria-label="素材占位名称"
        />
        <button className="material-draft-confirm" type="button" onMouseDown={(event) => event.preventDefault()} onClick={() => onConfirm(options[draft.activeIndex ?? 0])}>
          确定
        </button>
      </div>
      {options.length ? (
        <div className="material-draft-options" aria-label="素材占位候选">
          {options.map((option, index) => (
            <button
              className={clsx("material-draft-option", index === (draft.activeIndex ?? 0) && "active")}
              type="button"
              key={`${option.token}-${option.caption}`}
              onMouseDown={(event) => event.preventDefault()}
              onClick={() => onConfirm(option)}
            >
              <span className="chip-token">{option.token}</span>
              <span>{option.label}</span>
              <span>{option.caption}</span>
            </button>
          ))}
        </div>
      ) : null}
    </div>
  );
}

function materialTypeForAsset(asset: Asset): MaterialKind {
  const contentType = typeof asset.metadata.contentType === "string" ? asset.metadata.contentType.toLowerCase() : "";
  const url = asset.url.toLowerCase();
  if (asset.kind === "video" || contentType.startsWith("video/") || /\.(mp4|mov|webm)(\?|$)/.test(url)) return "video";
  if (contentType.startsWith("audio/") || /\.(mp3|wav|m4a|aac|flac)(\?|$)/.test(url)) return "audio";
  return "image";
}

function fileMatchesMaterialKind(file: File, type: MaterialKind): boolean {
  const contentType = file.type.toLowerCase();
  const name = file.name.toLowerCase();
  if (type === "image") return contentType.startsWith("image/") || /\.(png|jpe?g|webp|gif|avif)(\?|$)/.test(name);
  if (type === "video") return contentType.startsWith("video/") || /\.(mp4|mov|webm)(\?|$)/.test(name);
  return contentType.startsWith("audio/") || /\.(mp3|wav|m4a|aac|flac)(\?|$)/.test(name);
}

function acceptForMaterialKind(type: MaterialKind): string {
  if (type === "video") return "video/*";
  if (type === "audio") return "audio/*";
  return "image/*";
}

function labelFor(type: MaterialKind, index: number): string {
  if (type === "video") return `@视频${index}`;
  if (type === "audio") return `@音频${index}`;
  return `@图片${index}`;
}

function nextMaterialLabel(type: MaterialKind, existing: PromptMaterial[]): string {
  const used = new Set(existing.map((material) => normalizeMaterialLabel(material.label)).filter((label): label is string => Boolean(label)));
  let index = 1;
  while (used.has(labelFor(type, index))) index += 1;
  return labelFor(type, index);
}

function normalizeMaterialLabel(label?: string): string | undefined {
  const match = label?.trim().match(/^@(图片|图|视频|音频)(\d+)$/);
  if (!match) return undefined;
  const type = match[1] === "视频" ? "video" : match[1] === "音频" ? "audio" : "image";
  return labelFor(type, Number(match[2]));
}

function materialTypeForLabel(label: string): MaterialKind | undefined {
  const normalized = normalizeMaterialLabel(label);
  if (!normalized) return undefined;
  if (normalized.startsWith("@视频")) return "video";
  if (normalized.startsWith("@音频")) return "audio";
  return "image";
}

function materialIsReferenced(material: PromptMaterial, referencedLabels: Set<string>): boolean {
  const label = normalizeMaterialLabel(material.label);
  return Boolean(label && referencedLabels.has(label));
}

function findMentionAtCursor(value: string, cursor: number): MentionState | null {
  const beforeCursor = value.slice(0, cursor);
  const start = beforeCursor.lastIndexOf("@");
  if (start === -1) return null;
  const query = beforeCursor.slice(start + 1);
  if (query.length > 16 || /[\s\n\r，。；：、,.!?！？()[\]{}<>《》"'“”]/.test(query)) return null;
  if (/^(图片|图|视频|音频)\d*$/.test(query)) return null;
  return { start, query };
}

function materialMatchesMention(material: PromptMaterial, query: string): boolean {
  const normalizedQuery = normalizeMentionText(query);
  if (!normalizedQuery) return true;
  return [
    material.label,
    material.label?.replace(/^@/, ""),
    material.name,
    shortTypeName(material.type),
    material.type
  ].filter(Boolean).some((value) => normalizeMentionText(String(value)).includes(normalizedQuery));
}

function buildMaterialDraftOptions(input: { target: MaterialDraftTarget; query: string; prompt: string; materials: PromptMaterial[] }): MaterialDraftOption[] {
  const options: MaterialDraftOption[] = [];
  if (input.target === "prompt") {
    for (const material of input.materials) {
      const token = normalizeMaterialLabel(material.label);
      if (!token || !materialMatchesMention(material, input.query)) continue;
      options.push({
        token,
        label: material.name || token,
        caption: shortTypeName(material.type),
        type: material.type
      });
      if (options.length >= 5) break;
    }
  }

  for (const type of ["image", "video", "audio"] as MaterialDraftKind[]) {
    const token = nextMaterialDraftToken(type, input.prompt);
    const option = {
      token,
      label: `${shortTypeName(type)}占位`,
      caption: input.target === "template" ? "模板素材" : "待上传素材",
      type
    };
    if (materialDraftOptionMatches(option, input.query)) options.push(option);
  }

  return uniqueMaterialDraftOptions(options).slice(0, 8);
}

function materialDraftOptionMatches(option: MaterialDraftOption, query: string): boolean {
  const normalizedQuery = normalizeMentionText(query);
  if (!normalizedQuery) return true;
  return [option.token, option.token.replace(/^@/, ""), option.label, option.caption, shortTypeName(option.type), option.type]
    .some((value) => normalizeMentionText(value).includes(normalizedQuery));
}

function uniqueMaterialDraftOptions(options: MaterialDraftOption[]): MaterialDraftOption[] {
  const seen = new Set<string>();
  return options.filter((option) => {
    if (seen.has(option.token)) return false;
    seen.add(option.token);
    return true;
  });
}

function normalizeMentionText(value: string): string {
  return value.replace(/^@/, "").trim().toLowerCase();
}

function renderPromptTokenLayer(prompt: string, materials: PromptMaterial[]) {
  const materialByLabel = new Map(materials.map((material) => [normalizeMaterialLabel(material.label) || material.label, material]));
  const nodes: ReactNode[] = [];
  let cursor = 0;
  for (const match of prompt.matchAll(/@(图片|图|视频|音频)(\d+)/g)) {
    const index = match.index ?? 0;
    if (index > cursor) nodes.push(prompt.slice(cursor, index));
    const token = match[1] === "图" ? `@图片${match[2]}` : match[0];
    const material = materialByLabel.get(token);
    nodes.push(
      <span className={clsx("prompt-token-card", material ? "linked" : "unbound")} key={`${token}-${index}`}>
        {token}
      </span>
    );
    cursor = index + match[0].length;
  }
  if (cursor < prompt.length) nodes.push(prompt.slice(cursor));
  return nodes.length ? nodes : prompt;
}

function shortTypeName(type: MaterialKind) {
  if (type === "video") return "视频";
  if (type === "audio") return "音频";
  return "图片";
}
