<!-- 重绘工作台，负责编排源媒体、资产、分镜和生成任务。 -->
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref, watch } from "vue";
import { CircleClose, Document, Setting } from "@element-plus/icons-vue";
import { ElMessageBox } from "element-plus";
import { ArrowLeft } from "@/icons";
import { useRoute } from "vue-router";
import CreativeShell from "@/components/creative/CreativeShell.vue";
import CreativeSelect from "@/components/creative/CreativeSelect.vue";
import ModelConfigDialog from "@/components/creative/ModelConfigDialog.vue";
import RedrawAssetPanel from "@/components/creative/RedrawAssetPanel.vue";
import RedrawScriptPanel from "@/components/creative/RedrawScriptPanel.vue";
import RedrawSourcePanel from "@/components/creative/RedrawSourcePanel.vue";
import RedrawTextPanel from "@/components/creative/RedrawTextPanel.vue";
import StoryboardTimeline from "@/components/creative/StoryboardTimeline.vue";
import {
  creativeApi,
  type CreativeOptions,
  type GenerationTask,
  type ProjectAsset,
  type PromptPreset,
  type RedrawWorkbenchData,
  type Storyboard,
} from "@/api/creative";
import { apiError } from "@/api/http";
import { useToastStore } from "@/stores/toast";
import { taskElapsedLabel } from "@/utils/taskTimeout";

const route = useRoute();
const toast = useToastStore();
const projectId = String(route.params.projectId);
const episodeId = String(route.params.episodeId);
const data = ref<RedrawWorkbenchData>();
const tasks = ref<GenerationTask[]>([]);
const selectedId = ref("");
const loading = ref(false);
const sourceUploading = ref(false);
const subtitleUploading = ref(false);
const subtitleFileName = ref("");
const startingAnalysis = ref(false);
const startingReinfer = ref(false);
const startingScript = ref(false);
const scriptContent = ref("");
const savedScriptContent = ref("");
const savingScript = ref(false);
const scriptSaveState = ref<"idle" | "saved" | "error">("idle");
const assetBusy = ref(false);
const polling = ref(false);
const draggingVideo = ref(false);
const subtitleInput = ref<HTMLInputElement>();
const taskClock = ref(Date.now());
const suppressSettingsWatch = ref(false);
const persistingSettings = ref(false);
const options = ref<CreativeOptions>({ styles: [], models: [] });
const prompts = ref<PromptPreset[]>([]);
const promptSelections = ref<Record<string, string>>({});
const modelsOpen = ref(false);
const savingModels = ref(false);
const audioSource = ref("video_audio");
const sourceLanguage = ref("");
const promptTypes = ["剧本反推"];
const audioSourceOptions = [
  { label: "识别视频音轨", value: "video_audio" },
  { label: "使用字幕文件", value: "subtitle_file" },
];
const languageOptions = [
  { label: "自动识别", value: "" },
  { label: "中文", value: "zh" },
  { label: "英语", value: "en" },
  { label: "日语", value: "ja" },
  { label: "韩语", value: "ko" },
];
const activeStatuses = new Set([
  "pending_submission",
  "submitting",
  "submitted",
  "processing",
  "result_ready",
  "downloading",
  "cancel_requested",
]);
const selected = computed(() => data.value?.storyboards.find((item) => item.id === selectedId.value));
const analysisTask = computed(() =>
  tasks.value.find(
    (item) =>
      item.task_type === "prompt_reverse" &&
      item.mode !== "script" &&
      !item.storyboard_id &&
      activeStatuses.has(item.status),
  ),
);
const scriptTask = computed(
  () =>
    tasks.value
      .filter((item) => item.task_type === "prompt_reverse" && item.mode === "script")
      .sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at))[0],
);
const latestReverseTask = computed(() => {
  if (!selected.value) return undefined;
  return tasks.value
    .filter(
      (item) =>
        item.task_type === "prompt_reverse" &&
        item.mode !== "script" &&
        (item.storyboard_id === selected.value?.id ||
          (!item.storyboard_id && item.sequence_no === selected.value?.sequence_no)),
    )
    .sort((left, right) => Date.parse(right.created_at) - Date.parse(left.created_at))[0];
});
const reinferTask = computed(() =>
  latestReverseTask.value && activeStatuses.has(latestReverseTask.value.status) ? latestReverseTask.value : undefined,
);
const hasActiveStoryboardReverseTask = computed(() =>
  tasks.value.some(
    (item) =>
      item.task_type === "prompt_reverse" &&
      item.mode !== "script" &&
      (Boolean(item.storyboard_id) || item.sequence_no !== undefined) &&
      activeStatuses.has(item.status),
  ),
);
const hasAnalysisResult = computed(() =>
  Boolean(data.value?.source.source_video_url && data.value?.storyboards.length),
);
const scriptReady = computed(
  () =>
    data.value?.source.status === "review" &&
    Boolean(data.value.storyboards.length) &&
    data.value.storyboards.every((item) => Boolean(item.prompt_content?.trim())) &&
    !analysisTask.value &&
    !hasActiveStoryboardReverseTask.value,
);
const hasActiveTask = computed(
  () => tasks.value.some((item) => activeStatuses.has(item.status)) || data.value?.source.status === "analyzing",
);
const analysisButtonText = computed(() =>
  startingAnalysis.value
    ? "正在启动"
    : analysisTask.value?.status === "cancel_requested"
      ? "停止中"
      : analysisTask.value
        ? `停止：${data.value?.source.analysis_message || "正在反推"} ${taskElapsedLabel(analysisTask.value, taskClock.value)}`
        : hasAnalysisResult.value
          ? "全部重新反推"
          : "开始反推",
);
const analysisDisabled = computed(
  () =>
    startingAnalysis.value ||
    Boolean(scriptTask.value && activeStatuses.has(scriptTask.value.status)) ||
    analysisTask.value?.status === "cancel_requested" ||
    (audioSource.value === "subtitle_file" && !data.value?.source.subtitle_asset_id),
);

watch(
  data,
  (value) => {
    if (!value) return;
    suppressSettingsWatch.value = true;
    audioSource.value = value.source.audio_source || "video_audio";
    sourceLanguage.value = value.source.source_language || "";
    subtitleFileName.value = value.source.subtitle_original_name || "";
    if (!savingScript.value && scriptContent.value === savedScriptContent.value) {
      scriptContent.value = value.script_content || "";
      savedScriptContent.value = scriptContent.value;
    }
    if (!value.storyboards.some((item) => item.id === selectedId.value))
      selectedId.value = value.storyboards[0]?.id || "";
    queueMicrotask(() => (suppressSettingsWatch.value = false));
  },
  { deep: true },
);

let scriptSaveTimer: ReturnType<typeof setTimeout> | undefined;
let activeScriptSave: Promise<boolean> | undefined;
/** 安排短延迟自动保存，合并连续输入并避免每次按键都发起请求。 */
function scheduleScriptSave() {
  scriptSaveState.value = "idle";
  if (scriptSaveTimer) clearTimeout(scriptSaveTimer);
  scriptSaveTimer = setTimeout(() => void saveScript(), 400);
}
/** 串行保存最新剧本草稿；保存期间产生的新修改会在当前请求结束后继续提交。 */
function saveScript(): Promise<boolean> {
  if (scriptSaveTimer) clearTimeout(scriptSaveTimer);
  scriptSaveTimer = undefined;
  if (activeScriptSave) {
    return activeScriptSave.then((saved) =>
      saved && scriptContent.value !== savedScriptContent.value ? saveScript() : saved,
    );
  }
  if (scriptContent.value === savedScriptContent.value) return Promise.resolve(true);
  const submitted = scriptContent.value;
  savingScript.value = true;
  activeScriptSave = (async () => {
    try {
      await creativeApi.saveRedrawScript(projectId, episodeId, submitted);
      savedScriptContent.value = submitted;
      if (data.value) data.value.script_content = submitted;
      scriptSaveState.value = "saved";
      return true;
    } catch (error) {
      scriptSaveState.value = "error";
      showError(error, "自动保存剧本失败");
      return false;
    } finally {
      savingScript.value = false;
      activeScriptSave = undefined;
    }
  })();
  return activeScriptSave.then((saved) =>
    saved && scriptContent.value !== savedScriptContent.value ? saveScript() : saved,
  );
}

async function persistAnalysisSettings() {
  if (!data.value || persistingSettings.value || suppressSettingsWatch.value) return;
  if (
    (data.value.source.audio_source || "video_audio") === audioSource.value &&
    (data.value.source.source_language || "") === sourceLanguage.value
  )
    return;
  persistingSettings.value = true;
  try {
    await creativeApi.saveAnalysisSettings(projectId, episodeId, {
      audio_source: audioSource.value,
      source_language: sourceLanguage.value,
    });
  } catch (error) {
    showError(error, "保存台词设置失败");
  } finally {
    persistingSettings.value = false;
  }
}
watch([audioSource, sourceLanguage], () => void persistAnalysisSettings());

async function load(showLoading = true) {
  if (showLoading) loading.value = true;
  try {
    [data.value, tasks.value] = await Promise.all([
      creativeApi.redrawWorkbench(projectId, episodeId),
      creativeApi.tasks(projectId, episodeId),
    ]);
  } catch (error) {
    showError(error, "加载失败");
  } finally {
    if (showLoading) loading.value = false;
  }
}
async function loadModelOptions() {
  try {
    options.value = await creativeApi.options();
  } catch (error) {
    showError(error, "加载模型选项失败");
  }
}
async function openModels() {
  modelsOpen.value = true;
  try {
    const results = await Promise.all(promptTypes.map((type) => creativeApi.promptPresets(type)));
    const merged = new Map<string, PromptPreset>();
    const selectedPrompts: Record<string, string> = {};
    results.forEach((result, index) => {
      (result.prompts || []).forEach((prompt) => merged.set(prompt.id, prompt));
      if (result.selected_id) selectedPrompts[promptTypes[index]] = result.selected_id;
    });
    prompts.value = [...merged.values()];
    promptSelections.value = selectedPrompts;
  } catch (error) {
    showError(error, "加载提示词失败");
  }
}
async function saveModels(payload: Record<string, any>) {
  const value = payload.prompt_reverse;
  if (!value?.model_id) return toast.warning("请选择反推模型");
  savingModels.value = true;
  try {
    await creativeApi.saveModels(projectId, { prompt_reverse: value });
    modelsOpen.value = false;
    toast.success("模型配置已保存");
    await load(false);
  } catch (error) {
    showError(error, "保存失败");
  } finally {
    savingModels.value = false;
  }
}
async function selectPrompt(payload: { type: string; promptId: string }) {
  try {
    await creativeApi.selectPrompt(payload.type, payload.promptId);
    promptSelections.value = { ...promptSelections.value, [payload.type]: payload.promptId };
  } catch (error) {
    showError(error, "启用提示词失败");
  }
}
async function createPrompt(payload: { name: string; type: string; content: string }) {
  try {
    const saved = await creativeApi.createCustomPrompt(payload);
    prompts.value = [...prompts.value, saved];
    await creativeApi.selectPrompt(payload.type, saved.id);
    promptSelections.value = { ...promptSelections.value, [payload.type]: saved.id };
  } catch (error) {
    showError(error, "保存自定义提示词失败");
  }
}
async function updatePrompt(payload: { id: string; name: string; type: string; content: string }) {
  try {
    const saved = await creativeApi.updateCustomPrompt(payload.id, payload);
    prompts.value = prompts.value.map((item) => (item.id === saved.id ? saved : item));
  } catch (error) {
    showError(error, "保存提示词失败");
  }
}
async function deletePrompt(prompt: PromptPreset) {
  try {
    await ElMessageBox.confirm("删除后不可恢复，确定删除该提示词吗？", "删除提示词", {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
    await creativeApi.deleteCustomPrompt(prompt.id);
    prompts.value = prompts.value.filter((item) => item.id !== prompt.id);
  } catch (error) {
    if (error !== "cancel" && error !== "close") showError(error, "删除提示词失败");
  }
}
async function validateVideo(file: File) {
  const ext = file.name.toLowerCase().split(".").pop() || "";
  if (!["mp4", "webm", "mov", "m4v"].includes(ext)) {
    toast.error("不支持该视频格式");
    return false;
  }
  if (file.size > 50 * 1024 * 1024) {
    toast.error("视频不能超过 50MB");
    return false;
  }
  const url = URL.createObjectURL(file);
  try {
    const duration = await new Promise<number>((resolve, reject) => {
      const video = document.createElement("video");
      video.preload = "metadata";
      video.onloadedmetadata = () => resolve(video.duration);
      video.onerror = () => reject(new Error());
      video.src = url;
    });
    if (!Number.isFinite(duration) || duration <= 0 || duration > 180) {
      toast.error("视频时长不能超过 3 分钟");
      return false;
    }
    return true;
  } catch {
    toast.error("视频读取失败");
    return false;
  } finally {
    URL.revokeObjectURL(url);
  }
}
async function uploadSource(file: File) {
  if (sourceUploading.value || !(await validateVideo(file))) return;
  if (hasAnalysisResult.value) {
    try {
      await ElMessageBox.confirm("重新上传视频将删除当前剧本的分镜和提示词，资产会保留。是否继续？", "重新上传视频", {
        type: "warning",
        confirmButtonText: "继续上传",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }
  sourceUploading.value = true;
  try {
    await creativeApi.uploadSource(projectId, episodeId, file);
    toast.success("原视频已上传");
    await load(false);
  } catch (error) {
    showError(error, "上传失败");
  } finally {
    sourceUploading.value = false;
  }
}
function dropVideo(event: DragEvent) {
  draggingVideo.value = false;
  const file = event.dataTransfer?.files?.[0];
  if (file) void uploadSource(file);
}
async function uploadSubtitle(event: Event) {
  const input = event.target as HTMLInputElement;
  const file = input.files?.[0];
  input.value = "";
  if (!file) return;
  subtitleUploading.value = true;
  try {
    await creativeApi.uploadSubtitle(projectId, episodeId, file);
    subtitleFileName.value = file.name;
    audioSource.value = "subtitle_file";
    await load(false);
    await persistAnalysisSettings();
  } catch (error) {
    showError(error, "上传失败");
  } finally {
    subtitleUploading.value = false;
  }
}
async function startAnalysis() {
  if (analysisTask.value) return stopReverseTask(analysisTask.value);
  if (!data.value?.source.source_video_url) return toast.warning("请先上传原视频");
  if (hasAnalysisResult.value) {
    try {
      await ElMessageBox.confirm("全部重新反推将替换当前分镜提示词和剧本，资产名称会保留。是否继续？", "全部重新反推", {
        type: "warning",
        confirmButtonText: "继续",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }
  startingAnalysis.value = true;
  try {
    await creativeApi.saveAnalysisSettings(projectId, episodeId, {
      audio_source: audioSource.value,
      source_language: sourceLanguage.value,
    });
    const task = hasAnalysisResult.value
      ? await creativeApi.reanalyze(projectId, episodeId)
      : await creativeApi.analyze(projectId, episodeId);
    tasks.value.unshift(task);
    await load(false);
  } catch (error) {
    showError(error, "反推失败");
  } finally {
    startingAnalysis.value = false;
  }
}
async function saveAsset(assetId: string, payload: Partial<ProjectAsset>) {
  assetBusy.value = true;
  try {
    await creativeApi.updateAsset(projectId, assetId, { name: payload.name });
    await load(false);
  } catch (error) {
    showError(error, "保存失败");
  } finally {
    assetBusy.value = false;
  }
}
async function deleteAsset(asset: ProjectAsset) {
  try {
    await ElMessageBox.confirm(
      `确定删除资产“${asset.name}”吗？删除后，分镜提示词中的角色文字会保留，但 @ 标签会移除。`,
      "删除资产",
      {
        type: "warning",
        confirmButtonText: "删除",
        cancelButtonText: "取消",
      },
    );
    assetBusy.value = true;
    await creativeApi.deleteAsset(projectId, asset.id);
    await load(false);
    toast.success("资产已删除");
  } catch (error) {
    if (error !== "cancel" && error !== "close") showError(error, "删除资产失败");
  } finally {
    assetBusy.value = false;
  }
}
async function saveStoryboard(payload: Partial<Storyboard>) {
  if (!selected.value) return;
  try {
    await creativeApi.updateStoryboard(projectId, selected.value.id, payload);
    await load(false);
  } catch (error) {
    showError(error, "保存失败");
  }
}
async function insertStoryboard(storyboard: Storyboard, position: "before" | "after") {
  try {
    const created = await creativeApi.insertStoryboard(projectId, storyboard.id, position);
    selectedId.value = created.id;
    await load(false);
  } catch (error) {
    showError(error, "插入分镜失败");
  }
}
async function deleteStoryboard(storyboard: Storyboard) {
  try {
    await ElMessageBox.confirm(`确定删除分镜 ${storyboard.sequence_no} 吗？`, "删除分镜", {
      type: "warning",
      confirmButtonText: "删除",
      cancelButtonText: "取消",
    });
    await creativeApi.deleteStoryboard(projectId, storyboard.id);
    await load(false);
  } catch (error) {
    if (error !== "cancel" && error !== "close") showError(error, "删除分镜失败");
  }
}
/** 二次确认后重新反推当前分镜，取消时不创建任务。 */
async function reinferStoryboard() {
  if (!selected.value) return;
  try {
    await ElMessageBox.confirm("重新反推将仅替换当前分镜的反推结果，是否继续？", "重新反推当前分镜", {
      type: "warning",
      confirmButtonText: "继续反推",
      cancelButtonText: "取消",
    });
  } catch {
    return;
  }
  startingReinfer.value = true;
  try {
    tasks.value.unshift(await creativeApi.reinferStoryboard(projectId, episodeId, selected.value.id));
  } catch (error) {
    showError(error, "反推失败");
  } finally {
    startingReinfer.value = false;
  }
}
async function reverseScript() {
  if (!scriptReady.value) return;
  if (scriptContent.value.trim()) {
    try {
      await ElMessageBox.confirm("重新反推将替换当前完整剧本，是否继续？", "重新反推为剧本", {
        type: "warning",
        confirmButtonText: "继续反推",
        cancelButtonText: "取消",
      });
    } catch {
      return;
    }
  }
  if (!(await saveScript())) return;
  startingScript.value = true;
  try {
    const task = await creativeApi.reverseRedrawScript(projectId, episodeId);
    tasks.value.unshift(task);
    toast.success("完整剧本反推任务已开始");
    await load(false);
  } catch (error) {
    showError(error, "剧本反推失败");
  } finally {
    startingScript.value = false;
  }
}
async function stopReverseTask(task: GenerationTask) {
  try {
    await ElMessageBox.confirm("确定停止当前反推任务吗？迟到结果将被丢弃，已扣积分不退。", "停止反推", {
      type: "warning",
      confirmButtonText: "确认停止",
      cancelButtonText: "继续反推",
    });
  } catch (error) {
    if (error === "cancel" || error === "close") return;
    throw error;
  }
  try {
    task.status = "cancel_requested";
    await creativeApi.cancelTask(projectId, task.id);
    await load(false);
  } catch (error) {
    showError(error, "停止反推失败");
  }
}
async function refreshTasks() {
  if (polling.value) return;
  polling.value = true;
  try {
    const previous = new Map(tasks.value.map((item) => [item.id, item.status]));
    const next = await creativeApi.tasks(projectId, episodeId);
    const changed =
      next.map((item) => `${item.id}:${item.status}`).join("|") !==
      tasks.value.map((item) => `${item.id}:${item.status}`).join("|");
    tasks.value = next;
    const failedTasks = next.filter(
      (item) => item.task_type === "prompt_reverse" && item.status === "failed" && previous.get(item.id) !== "failed",
    );
    for (const task of failedTasks) {
      const title = task.mode === "script" ? "剧本反推失败" : task.storyboard_id ? "分镜反推失败" : "视频反推失败";
      showTaskFailure(task, title);
    }
    if (changed || hasActiveTask.value) data.value = await creativeApi.redrawWorkbench(projectId, episodeId);
  } finally {
    polling.value = false;
  }
}
function showTaskFailure(task: GenerationTask, title: string) {
  const message = task.error_message?.trim() || title;
  const detail = [`错误码：${task.error_code || "task_failed"}`, `错误描述：${message}`, `任务 ID：${task.id}`].join(
    "\n",
  );
  toast.error(message, detail, title);
}
function showError(error: unknown, title: string) {
  const detail = apiError(error);
  toast.error(detail.message, detail.copyText, title);
}
let timer: ReturnType<typeof setTimeout> | undefined;
let clockTimer: ReturnType<typeof setInterval> | undefined;
let disposed = false;
async function pollTasks() {
  await refreshTasks();
  if (!disposed) timer = setTimeout(pollTasks, hasActiveTask.value ? 1000 : 5000);
}
onMounted(async () => {
  await Promise.all([load(), loadModelOptions()]);
  timer = setTimeout(pollTasks, 1000);
  clockTimer = setInterval(() => (taskClock.value = Date.now()), 1000);
});
onBeforeUnmount(() => {
  disposed = true;
  if (timer) clearTimeout(timer);
  if (clockTimer) clearInterval(clockTimer);
  if (scriptSaveTimer) clearTimeout(scriptSaveTimer);
  if (scriptContent.value !== savedScriptContent.value) void saveScript();
});
</script>

<template>
  <CreativeShell
    class="workbench-shell"
    @dragenter.prevent="draggingVideo = true"
    @dragover.prevent="draggingVideo = true"
    @dragleave.prevent="draggingVideo = false"
    @drop.prevent="dropVideo"
  >
    <section class="analysis-bar">
      <div class="analysis-controls">
        <RouterLink
          class="analysis-back"
          :to="{ name: 'redraw-project', params: { projectId } }"
          ><ArrowLeft :size="18"
        /></RouterLink>
        <label
          ><span>台词来源</span
          ><CreativeSelect
            v-model="audioSource"
            :options="audioSourceOptions"
        /></label>
        <label v-if="audioSource === 'video_audio'"
          ><span>源语言</span
          ><CreativeSelect
            v-model="sourceLanguage"
            :options="languageOptions"
            clearable
            placeholder="自动识别"
        /></label>
        <input
          v-if="audioSource === 'subtitle_file'"
          ref="subtitleInput"
          class="source-file-input"
          type="file"
          accept=".srt,.vtt,.txt"
          :disabled="subtitleUploading"
          @change="uploadSubtitle"
        />
        <el-button
          v-if="audioSource === 'subtitle_file'"
          :icon="Document"
          :loading="subtitleUploading"
          @click="subtitleInput?.click()"
          >{{ subtitleFileName || "上传字幕" }}</el-button
        >
      </div>
      <div class="analysis-actions creative-page-actions">
        <el-button
          :icon="Setting"
          @click="openModels"
          >模型配置</el-button
        >
        <el-button
          :type="analysisTask ? 'danger' : 'primary'"
          :icon="analysisTask ? CircleClose : undefined"
          :disabled="analysisDisabled"
          :loading="startingAnalysis"
          @click="startAnalysis"
          >{{ analysisButtonText }}</el-button
        >
      </div>
    </section>
    <div
      v-loading="loading"
      class="workbench-grid"
    >
      <RedrawSourcePanel
        :source="data?.source"
        :uploading="sourceUploading"
        :dragging="draggingVideo"
        @upload="uploadSource"
      />
      <RedrawAssetPanel
        :assets="data?.assets || []"
        :busy="assetBusy"
        @save="saveAsset"
        @delete="deleteAsset"
      />
      <RedrawTextPanel
        :storyboards="data?.storyboards || []"
        :assets="data?.assets || []"
        :selected-id="selectedId"
        :reinfer-task="reinferTask"
        :now="taskClock"
        :reinferring="startingReinfer"
        @save="saveStoryboard"
        @reinfer="reinferStoryboard"
        @stop-reinfer="stopReverseTask"
      />
      <RedrawScriptPanel
        :content="scriptContent"
        :ready="scriptReady"
        :task="scriptTask"
        :starting="startingScript"
        :saving="savingScript"
        :save-state="scriptSaveState"
        :now="taskClock"
        @update:content="
          (content) => {
            scriptContent = content;
            scheduleScriptSave();
          }
        "
        @save="saveScript"
        @reverse="reverseScript"
        @stop="stopReverseTask"
      />
    </div>
    <StoryboardTimeline
      :storyboards="data?.storyboards || []"
      :selected-id="selectedId"
      :tasks="tasks"
      @select="(item) => (selectedId = item.id)"
      @insert="insertStoryboard"
      @delete="deleteStoryboard"
    />
    <ModelConfigDialog
      v-if="data?.project"
      v-model="modelsOpen"
      analysis-only
      :options="options"
      :configs="data.project.model_configs"
      :prompts="prompts"
      :prompt-selections="promptSelections"
      :saving="savingModels"
      @save="saveModels"
      @prompt-select="selectPrompt"
      @prompt-create="createPrompt"
      @prompt-update="updatePrompt"
      @prompt-delete="deletePrompt"
    />
  </CreativeShell>
</template>

<style scoped lang="scss">
.workbench-shell {
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
.workbench-shell :deep(.creative-shell__scroll) {
  min-height: 0;
  padding-bottom: 16px;
  display: flex;
  flex: 1;
  flex-direction: column;
  overflow: hidden;
}
.source-file-input {
  display: none;
}
.analysis-bar {
  margin-bottom: var(--space-md);
  padding: var(--space-sm) var(--space-md);
  display: grid;
  grid-template-columns: minmax(0, 1fr) auto;
  align-items: center;
  gap: var(--space-lg);
  background: var(--web-surface);
  border: 1px solid var(--web-line);
  border-radius: var(--radius-lg);
}
.analysis-controls,
.analysis-actions {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 9px;
}
.analysis-controls {
  flex-wrap: wrap;
}
.analysis-actions {
  justify-self: end;
}
.analysis-controls label {
  min-width: 0;
  display: flex;
  align-items: center;
  gap: 7px;
}
.analysis-controls label > span {
  flex: 0 0 auto;
  color: var(--web-text-secondary);
  font-size: 12px;
}
.analysis-controls .creative-select {
  width: 150px;
}
.analysis-controls :deep(.el-button) {
  max-width: 220px;
}
.analysis-controls :deep(.el-button > span) {
  min-width: 0;
  overflow: hidden;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.analysis-back {
  width: 36px;
  height: 36px;
  display: grid;
  place-items: center;
  flex: 0 0 36px;
  color: var(--web-text-secondary);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 7px;
}
.workbench-grid {
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(220px, 0.9fr) minmax(220px, 0.9fr) minmax(300px, 1.25fr) minmax(280px, 1.1fr);
  flex: 1;
  gap: var(--space-md);
}
.workbench-grid + :deep(.timeline-panel) {
  margin-top: var(--space-md);
  flex: 0 0 auto;
}
.workbench-grid > :deep(.redraw-column) {
  min-height: 0;
  background: var(--web-surface);
  border: 1px solid var(--web-line);
  border-radius: var(--radius-lg);
  overflow: hidden;
}
.workbench-grid > :deep(.redraw-column > header) {
  height: 42px;
  padding: 0 13px;
  display: flex;
  align-items: center;
  gap: 7px;
  flex: 0 0 42px;
  border-bottom: 1px solid var(--web-line-strong);
}
.workbench-grid > :deep(.redraw-column > header small) {
  color: var(--web-script-hint);
  font-size: 11px;
}
@media (max-width: 1400px) {
  .workbench-grid {
    grid-template-columns: repeat(2, minmax(0, 1fr));
    overflow: auto;
  }
  .workbench-grid > :deep(.redraw-column) {
    min-height: 420px;
  }
}
@media (max-width: 900px) {
  .workbench-shell,
  .workbench-shell :deep(.creative-shell__scroll) {
    height: auto;
    overflow: visible;
  }
  .analysis-bar {
    display: flex;
    align-items: stretch;
    flex-direction: column;
  }
  .analysis-controls,
  .analysis-actions {
    align-items: stretch;
    flex-direction: column;
  }
  .analysis-controls label {
    align-items: stretch;
    flex-direction: column;
  }
  .analysis-controls .creative-select {
    width: 100%;
  }
  .workbench-grid {
    grid-template-columns: 1fr;
    overflow: visible;
  }
  .workbench-grid > :deep(.redraw-column) {
    min-height: 360px;
  }
}
</style>
