<!-- 分镜提示词面板，负责编辑、保存和重新推理分镜提示词。 -->
<script setup lang="ts">
import { computed, ref, watch } from "vue";
import { CircleClose, FullScreen, Refresh, VideoPlay } from "@element-plus/icons-vue";
import type { GenerationTask, ProjectAsset, Storyboard } from "@/api/creative";
import AssetPromptEditor from "@/components/creative/AssetPromptEditor.vue";
import { taskElapsedLabel } from "@/utils/taskTimeout";
const props = defineProps<{
  storyboard?: Storyboard;
  assets: ProjectAsset[];
  task?: GenerationTask;
  reinferTask?: GenerationTask;
  now?: number;
  generating?: boolean;
  reinferring?: boolean;
  historyCount?: number;
}>();
const emit = defineEmits<{
  save: [payload: Partial<Storyboard>];
  generate: [payload: Partial<Storyboard>];
  reinfer: [];
  stopReinfer: [task: GenerationTask];
  stop: [];
  history: [];
}>();
const promptContent = ref(""),
  expanded = ref(false),
  audioDurations = ref<Record<string, number>>({});
const active = computed(
  () =>
    props.task &&
    [
      "pending_submission",
      "submitting",
      "submitted",
      "processing",
      "result_ready",
      "downloading",
      "cancel_requested",
    ].includes(props.task.status),
);
const stoppable = computed(
  () => props.task && ["pending_submission", "submitted", "processing"].includes(props.task.status),
);
const reinferActive = computed(() =>
  Boolean(
    props.reinferTask &&
    [
      "pending_submission",
      "submitting",
      "submitted",
      "processing",
      "result_ready",
      "downloading",
      "cancel_requested",
    ].includes(props.reinferTask.status),
  ),
);
const reinferStopping = computed(() => props.reinferTask?.status === "cancel_requested");
const reinferBusy = computed(() => Boolean(props.reinferring) || reinferActive.value);
const reinferLabel = computed(() =>
  props.reinferring
    ? "正在启动"
    : reinferStopping.value
      ? "停止中"
      : props.reinferTask
        ? `停止反推 ${taskElapsedLabel(props.reinferTask, props.now || Date.now())}`
        : "重新反推当前分镜",
);
const referencedAssets = computed(() =>
  props.assets.filter((asset) => asset.name.trim() && promptContent.value.includes(asset.name.trim())),
);
const imageReferenceCount = computed(() => referencedAssets.value.filter((asset) => Boolean(asset.image_url)).length);
const audioReferences = computed(() => referencedAssets.value.filter((asset) => Boolean(asset.audio_url)));
const audioReferenceDuration = computed(
  () =>
    audioReferences.value.reduce(
      (total, asset) => total + (Number(asset.audio_duration_ms) || audioDurations.value[asset.id] || 0),
      0,
    ) / 1000,
);
const referenceLimitReason = computed(() => {
  if (imageReferenceCount.value > 9) return "参考图数量超限";
  if (audioReferences.value.length > 3) return "音频数量超限";
  if (audioReferenceDuration.value > 15) return "音频时长超限";
  if (audioReferences.value.length > 0 && imageReferenceCount.value === 0) return "禁止单独参考音频";
  return "";
});
const videoLabel = computed(() =>
  props.task?.status === "cancel_requested"
    ? "停止中"
    : active.value
      ? `停止生成 ${taskElapsedLabel(props.task, props.now || Date.now())}`
      : referenceLimitReason.value || ((props.historyCount || 0) >= 5 ? "历史已满" : "生成视频"),
);
watch(
  () => props.storyboard,
  (value) => {
    promptContent.value = value?.prompt_content || "";
  },
  { immediate: true },
);
watch(
  () => props.assets,
  (assets) => {
    assets
      .filter((asset) => asset.audio_url && asset.audio_duration_ms == null && !audioDurations.value[asset.id])
      .forEach((asset) => {
        const audio = new Audio();
        audio.preload = "metadata";
        audio.onloadedmetadata = () => {
          if (Number.isFinite(audio.duration))
            audioDurations.value = { ...audioDurations.value, [asset.id]: Math.round(audio.duration * 1000) };
        };
        audio.src = asset.audio_url || "";
      });
  },
  { immediate: true, deep: true },
);
/** 组装当前编辑状态对应的分镜更新载荷。 */
function payload(): Partial<Storyboard> {
  const refs = props.assets
    .filter((asset) => asset.name.trim() && promptContent.value.includes(asset.name.trim()))
    .map((asset) => ({ id: asset.id, name: asset.name }));
  return { prompt_content: promptContent.value, asset_refs: refs };
}
/** 提交当前分镜提示词修改。 */
function save() {
  if (props.storyboard && promptContent.value !== (props.storyboard.prompt_content || "")) emit("save", payload());
}
/** 保存当前内容后触发重新推理。 */
function handleReinfer() {
  if (props.reinferTask) emit("stopReinfer", props.reinferTask);
  else emit("reinfer");
}
</script>
<template>
  <aside class="prompt-panel">
    <header><strong>分镜提示词</strong></header>
    <div class="prompt-panel-body">
      <template v-if="storyboard"
        ><div class="prompt-editor-wrap">
          <AssetPromptEditor
            v-model="promptContent"
            :assets="assets"
            placeholder="输入提示词，输入 @ 插入资产"
            @blur="save"
          /><button
            class="expand-button"
            type="button"
            title="放大编辑提示词"
            @click="expanded = true"
          >
            <el-icon><FullScreen /></el-icon>
          </button>
        </div>
        <div class="prompt-actions">
          <el-button
            :type="reinferActive ? 'danger' : 'default'"
            :icon="reinferActive ? CircleClose : Refresh"
            :loading="Boolean(reinferring)"
            :disabled="active || generating || reinferStopping || Boolean(reinferring)"
            @click="handleReinfer"
            >{{ reinferLabel }}</el-button
          ><el-button
            v-if="active || (!reinferBusy && Boolean(promptContent.trim()))"
            :type="active ? 'danger' : 'primary'"
            :icon="active ? CircleClose : VideoPlay"
            :loading="!active && generating"
            :disabled="active ? !stoppable : (historyCount || 0) >= 5 || Boolean(referenceLimitReason)"
            @click="active ? emit('stop') : emit('generate', payload())"
            >{{ videoLabel }}</el-button
          >
        </div></template
      >
      <div
        v-else
        class="prompt-empty"
      >
        从下方时间线选择一个分镜。
      </div>
    </div>
    <el-dialog
      v-model="expanded"
      title="提示词"
      width="min(900px, calc(100vw - 28px))"
      class="web-dialog creative-dialog prompt-dialog"
      append-to-body
      align-center
      @closed="save"
      ><AssetPromptEditor
        v-model="promptContent"
        :assets="assets"
        placeholder="输入提示词，输入 @ 插入资产"
        expanded
        @blur="save"
    /></el-dialog>
  </aside>
</template>
<style scoped lang="scss">
.prompt-panel {
  min-width: 0;
  display: flex;
  flex-direction: column;
  background: var(--web-surface-soft);
  border: 1px solid var(--web-line-strong);
  border-radius: 11px;
  overflow: hidden;
}
.prompt-panel > header {
  height: 40px;
  padding: 0 14px;
  display: flex;
  align-items: center;
  flex: 0 0 40px;
  border-bottom: 1px solid var(--web-line-strong);
}
.prompt-panel-body {
  min-height: 0;
  padding: 14px;
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 12px;
  overflow: hidden;
}
.prompt-editor-wrap {
  position: relative;
  min-height: 0;
  flex: 1;
  overflow: hidden;
}
.expand-button {
  position: absolute;
  right: 9px;
  bottom: 9px;
  width: 28px;
  height: 28px;
  display: grid;
  place-items: center;
  color: var(--web-panel-control-text);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 6px;
  cursor: pointer;
}
.expand-button:hover {
  color: var(--web-accent-dark);
  border-color: var(--web-line-emphasis);
}
.expand-button:focus-visible {
  outline: 2px solid var(--web-primary);
  outline-offset: 2px;
}
.prompt-actions {
  display: grid;
  grid-template-columns: 1fr 1.25fr;
  gap: 8px;
}
.prompt-empty {
  min-height: 0;
  display: grid;
  place-items: center;
  flex: 1;
  color: var(--web-text-tertiary);
  font-size: 13px;
}
</style>
<style lang="scss">
.prompt-dialog.el-dialog {
  height: min(720px, calc(100dvh - 40px));
  display: flex;
  flex-direction: column;
}
.prompt-dialog .el-dialog__header {
  flex: 0 0 auto;
}
.prompt-dialog .el-dialog__body {
  min-height: 0;
  display: flex;
  flex: 1;
  overflow: hidden;
}
.prompt-dialog .asset-prompt-editor {
  min-height: 0;
  height: 100%;
  flex: 1;
  overflow: hidden;
}
.prompt-dialog .asset-prompt-editor.expanded .prompt-content {
  min-height: 0;
  overflow: auto;
}
</style>
