<!-- 图片生成生图工具，负责临时配置、参考图提及、生成历史和图片预览。 -->
<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { ElMessageBox } from "element-plus";
import { Image, LoaderCircle, Sparkles, Trash2, Upload, X } from "@/icons";
import CreativeSelect from "@/components/creative/CreativeSelect.vue";
import CreativeShell from "@/components/creative/CreativeShell.vue";
import AssetPromptEditor from "@/components/creative/AssetPromptEditor.vue";
import LoadingMedia from "@/components/creative/LoadingMedia.vue";
import ProductImageViewer, { type ProductViewerImage } from "@/components/creative/ProductImageViewer.vue";
import { creativeApi, type CreativeOptions, type ProjectAsset } from "@/api/creative";
import { productImageApi, type ProductImageGeneration } from "@/api/productImage";
import { apiError } from "@/api/http";
import { useToastStore } from "@/stores/toast";
import { formatDateTime } from "@/utils/datetime";

interface LocalReference {
  id: string;
  file: File;
  name: string;
  url: string;
}

const toast = useToastStore();
const options = ref<CreativeOptions>({ styles: [], models: [], prices: [] });
const prompt = ref("");
const modelId = ref("");
const aspectRatio = ref("9:21");
const resolution = ref("1k");
const references = ref<LocalReference[]>([]);
const generations = ref<ProductImageGeneration[]>([]);
const loading = ref(true);
const generating = ref(false);
const previewIndex = ref(-1);
const referenceBusy = ref(false);
const fileInput = ref<HTMLInputElement>();
let pollingTimer: number | undefined;

const imageModels = computed(() =>
  options.value.models
    .filter((model) => model.model_type === "image")
    .map((model) => ({
      label: model.channel_name ? `【${model.channel_name}】${model.name}` : model.name,
      value: model.id,
    })),
);
const ratioOptions = ["1:1", "3:4", "4:3", "9:16", "9:21", "16:9"].map((value) => ({ label: value, value }));
const resolutionOptions = ["1k", "2k", "4k"].map((value) => ({ label: value, value }));
const completedImages = computed(() => generations.value.filter((item) => item.public_url));
const viewerImages = computed<ProductViewerImage[]>(() =>
  [
    ...completedImages.value.map((item) => ({
      id: item.task_id,
      mediaId: item.media_asset_id,
      url: item.public_url,
      prompt: item.prompt,
    })),
    ...references.value.map((reference) => ({
      id: reference.id,
      url: reference.url,
      prompt: reference.name,
      filename: reference.name,
    })),
  ],
);
const previewItem = computed(() => viewerImages.value[previewIndex.value]);
const previewOpen = computed({
  get: () => previewIndex.value >= 0,
  set: (value: boolean) => { if (!value) closePreview(); },
});
const hasActiveTask = computed(() => generations.value.some((item) => activeStatuses.has(item.status)));
const promptLength = computed(() => [...prompt.value].length);
const price = computed(() => {
  const selected = options.value.prices?.find(
    (item) => item.model_id === modelId.value && item.price_key.toLowerCase() === resolution.value.toLowerCase(),
  );
  return selected ? Number(selected.price).toFixed(2) : "";
});
const activeStatuses = new Set([
  "pending_submission",
  "submitting",
  "submitted",
  "processing",
  "result_ready",
  "downloading",
  "cancel_requested",
]);

const referenceAssets = computed<ProjectAsset[]>(() =>
  references.value.map((reference) => ({
    id: reference.id,
    project_id: "product-images",
    asset_type: "custom",
    name: reference.name,
    image_url: reference.url,
    user_edited: false,
  })),
);

/** 加载可用模型和用户的图片生成历史；提示词与参考图刻意从空状态开始。 */
async function load() {
  loading.value = true;
  try {
    const [creativeOptions, history] = await Promise.all([creativeApi.options(), productImageApi.generations()]);
    options.value = creativeOptions;
    generations.value = history;
    if (!modelId.value) modelId.value = imageModels.value[0]?.value || "";
    syncPolling();
  } catch (error) {
    showError(error, "加载图片生成失败");
  } finally {
    loading.value = false;
  }
}

/** 打开本地文件选择器；文件只保留在当前页面内存中。 */
function chooseReference() {
  if (!generating.value && references.value.length < 4) fileInput.value?.click();
}

/** 接收参考图并使用文件名作为 @ 提及候选名称。 */
function addReferences(event: Event) {
  const input = event.target as HTMLInputElement;
  const files = Array.from(input.files || []);
  input.value = "";
  if (!files.length) return;
  const existingNames = new Set(references.value.map((item) => item.name));
  for (const file of files) {
    if (references.value.length >= 4) break;
    if (!file.type.startsWith("image/")) {
      toast.warning("仅支持上传图片");
      continue;
    }
    if (existingNames.has(file.name)) {
      toast.warning(`参考图名称重复：${file.name}`);
      continue;
    }
    existingNames.add(file.name);
    references.value.push({
      id: `${Date.now()}-${Math.random()}`,
      file,
      name: file.name,
      url: URL.createObjectURL(file),
    });
  }
}

/** 删除临时参考图，并移除提示词中对应的文件名提及。 */
function removeReference(id: string) {
  const index = references.value.findIndex((item) => item.id === id);
  if (index < 0) return;
  const [removed] = references.value.splice(index, 1);
  URL.revokeObjectURL(removed.url);
  prompt.value = prompt.value.replaceAll(`@${removed.name}`, "");
}

/** 提交当前页面配置和参考图；任务受理后立即释放提交状态，并继续刷新生成历史。 */
async function generate() {
  if (!prompt.value.trim()) {
    toast.warning("请输入图片生成提示词");
    return;
  }
  if (promptLength.value > 5000) {
    toast.warning("提示词不能超过 5000 个字符");
    return;
  }
  if (!modelId.value) {
    toast.warning("请选择图片模型");
    return;
  }
  generating.value = true;
  try {
    await productImageApi.generate({
      prompt: prompt.value,
      model_id: modelId.value,
      aspect_ratio: aspectRatio.value,
      resolution: resolution.value,
      references: references.value.map((item) => item.file),
    });
    generating.value = false;
    toast.success("图片生成任务已提交");
    await refreshGenerations();
  } catch (error) {
    showError(error, "提交生成失败");
  } finally {
    generating.value = false;
  }
}

/** 刷新生成历史，避免覆盖当前尚未提交的输入。 */
async function refreshGenerations() {
  try {
    generations.value = await productImageApi.generations();
    syncPolling();
  } catch (error) {
    stopPolling();
    showError(error, "刷新效果图失败");
  }
}

/** 根据任务状态启动或停止轮询。 */
function syncPolling() {
  if (hasActiveTask.value && pollingTimer === undefined) pollingTimer = window.setInterval(refreshGenerations, 4000);
  else if (!hasActiveTask.value) stopPolling();
}

/** 停止图片生成任务轮询。 */
function stopPolling() {
  if (pollingTimer !== undefined) window.clearInterval(pollingTimer);
  pollingTimer = undefined;
}

/** 删除一条历史结果并让后端同步清理 COS 对象。 */
async function removeGeneration(item: ProductImageGeneration) {
  try {
    const target = item.public_url ? "效果图" : "生成记录";
    await ElMessageBox.confirm(`删除后无法恢复，确定删除这条${target}吗？`, `删除${target}`, {
      confirmButtonText: "删除",
      cancelButtonText: "取消",
      type: "warning",
    });
    await productImageApi.removeGeneration(item.task_id);
    generations.value = generations.value.filter((candidate) => candidate.task_id !== item.task_id);
    closePreview();
    toast.success("效果图已删除");
  } catch (error) {
    if (error !== "cancel" && error !== "close") showError(error, "删除效果图失败");
  }
}

/** 打开指定效果图的全屏预览。 */
function openPreview(item: ProductImageGeneration) {
  const index = viewerImages.value.findIndex((candidate) => candidate.id === item.task_id);
  if (index >= 0) {
    previewIndex.value = index;
  }
}
/** 打开指定参考图的大图查看器。 */
function openReferencePreview(reference: LocalReference) {
  const index = viewerImages.value.findIndex((candidate) => candidate.id === reference.id);
  if (index >= 0) previewIndex.value = index;
}
/** 关闭全屏预览并重置提示词面板。 */
function closePreview() {
  previewIndex.value = -1;
}
/** 将大图查看器生成的文件加入当前参考图列表。 */
function addReferenceFile(file: File) {
  if (references.value.length >= 4) { toast.warning("参考图最多 4 张"); return; }
  const name = file.name || `参考图-${Date.now()}.png`;
  references.value.push({ id: `${Date.now()}-${Math.random()}`, file, name, url: URL.createObjectURL(file) });
  toast.success("已加入参考图");
}

/** 释放当前页面创建的本地对象 URL。 */
function releaseReferences() {
  references.value.forEach((reference) => URL.revokeObjectURL(reference.url));
  references.value = [];
}
/** 将接口异常转换为统一的图片生成提示。 */
function showError(error: unknown, title: string) {
  const detail = apiError(error);
  toast.error(detail.message, detail.copyText, title);
}

onMounted(load);
onBeforeUnmount(() => {
  stopPolling();
  releaseReferences();
});
</script>

<template>
  <CreativeShell class="product-workbench-shell">
    <div class="product-workbench">
      <section class="panel-block product-settings">
        <header class="panel-block__header"><strong>生成设置</strong></header>
        <div
          v-loading="loading"
          class="product-settings__body"
        >
          <label class="product-field product-field--prompt">
            <span>提示词</span>
            <div class="prompt-editor-wrap">
              <AssetPromptEditor
                v-model="prompt"
                :assets="referenceAssets"
                variant="drama-storyboard"
                floating-preview
                placeholder="输入提示词，输入 @ 插入参考图"
              />
              <small class="prompt-count">{{ promptLength }} / 5000</small>
            </div>
          </label>
          <section class="reference-section">
            <header>
              <strong>参考图</strong><small>{{ references.length }} / 4 张</small>
            </header>
            <div class="reference-grid">
              <div
                v-for="reference in references"
                :key="reference.id"
                class="reference-item"
                @click="openReferencePreview(reference)"
              >
                <img
                  :src="reference.url"
                  :alt="reference.name"
                />
                <button
                  type="button"
                  :title="`删除 ${reference.name}`"
                  @click.stop="removeReference(reference.id)"
                >
                  <X :size="16" />
                </button>
              </div>
              <button
                type="button"
                class="reference-add"
                :disabled="references.length >= 4"
                @click="chooseReference"
              >
                <Upload :size="22" /><span>添加图片</span>
              </button>
            </div>
            <input
              ref="fileInput"
              type="file"
              accept="image/jpeg,image/png,image/webp"
              multiple
              hidden
              @change="addReferences"
            />
          </section>
          <section class="generation-parameters">
            <strong>生成参数</strong>
            <div class="generation-parameters__grid">
              <CreativeSelect
                v-model="aspectRatio"
                :options="ratioOptions"
                placement="top-start"
                placeholder="比例"
              />
              <CreativeSelect
                v-model="resolution"
                :options="resolutionOptions"
                placement="top-start"
                placeholder="分辨率"
              />
            </div>
            <CreativeSelect
              v-model="modelId"
              :options="imageModels"
              placement="top-start"
              placeholder="请选择图片模型"
            />
          </section>
        </div>
        <footer class="product-settings__footer">
          <el-button
            type="primary"
            :loading="generating"
            :disabled="loading"
            @click="generate"
          >
            <span class="generation-button__content">
              <span class="generation-button__action"><Sparkles :size="17" /><strong>开始生成</strong></span>
              <small
                v-if="price"
                class="generation-button__price"
                ><b>{{ price }}</b
                ><span>积分</span></small
              >
            </span>
          </el-button>
        </footer>
      </section>

      <section class="panel-block product-results">
        <header class="panel-block__header">
          <strong>效果图</strong><small>{{ generations.length }} 条</small>
        </header>
        <div
          v-loading="loading"
          class="product-results__body"
        >
          <article
            v-for="item in generations"
            :key="item.task_id"
            class="result-card"
            :class="{ 'has-image': item.public_url }"
          >
            <button
              type="button"
              class="result-card__preview"
              :disabled="!item.public_url"
              @click="openPreview(item)"
            >
              <LoadingMedia
                v-if="item.public_url"
                type="image"
                :src="item.public_url"
                alt="图片生成效果"
              />
              <span
                v-else
                class="result-card__state"
                ><LoaderCircle
                  v-if="activeStatuses.has(item.status)"
                  class="is-spinning"
                  :size="26"
                /><Image
                  v-else
                  :size="26"
                /><b>{{
                  activeStatuses.has(item.status) ? (item.error_message ? "结果同步中" : "生成中") : "生成失败"
                }}</b
                ><small
                  v-if="item.error_message"
                  :title="item.error_message"
                  >{{ item.error_message }}</small
                ></span
              >
              <span class="result-card__meta"
                ><strong>{{ item.model_name || "图片模型" }}</strong
                ><time>{{ formatDateTime(item.created_at) }}</time></span
              >
            </button>
            <button
              v-if="!activeStatuses.has(item.status) || item.error_message"
              type="button"
              class="result-card__delete"
              :title="item.public_url ? '删除效果图' : '删除生成记录'"
              @click.stop="removeGeneration(item)"
            >
              <Trash2 :size="17" />
            </button>
          </article>
          <div
            v-if="!loading && !generations.length"
            class="product-results__empty"
          >
            <Image :size="40" /><strong>还没有效果图</strong>
            <p>完成左侧配置后开始生成。</p>
          </div>
        </div>
      </section>
    </div>

    <ProductImageViewer
      v-model="previewOpen"
      :images="viewerImages"
      :initial-id="previewItem?.id"
      :reference-busy="referenceBusy"
      @reference="addReferenceFile"
    />
  </CreativeShell>
</template>

<style scoped lang="scss">
.product-workbench-shell {
  height: 100dvh;
  min-height: 0;
  overflow: hidden;
}
.product-workbench-shell :deep(.creative-shell__scroll) {
  min-height: 0;
  padding-bottom: 20px;
  display: flex;
  flex: 1;
  flex-direction: column;
  overflow: hidden;
}
.product-workbench {
  min-height: 0;
  display: grid;
  grid-template-columns: minmax(300px, 360px) minmax(0, 1fr);
  flex: 1;
  gap: 14px;
}
.panel-block {
  min-width: 0;
  background: var(--web-surface-soft);
  border: 1px solid var(--web-line-strong);
  border-radius: 13px;
  overflow: hidden;
}
.panel-block__header {
  min-height: 56px;
  padding: 0 18px;
  display: flex;
  align-items: center;
  gap: 8px;
  border-bottom: 1px solid var(--web-line-strong);
}
.panel-block__header strong {
  font-size: 16px;
}
.panel-block__header small {
  color: var(--web-muted);
  font-size: 12px;
}
.product-settings {
  min-height: 620px;
  display: flex;
  flex-direction: column;
}
.product-settings__body {
  min-height: 0;
  padding: 18px;
  display: flex;
  flex: 1;
  flex-direction: column;
  gap: 18px;
  overflow-y: auto;
}
.product-field {
  min-width: 0;
  display: flex;
  flex-direction: column;
  gap: 7px;
}
.product-field > span,
.reference-section header strong,
.generation-parameters > strong {
  color: var(--web-text-secondary);
  font-size: 12px;
  font-weight: 600;
}
.prompt-editor-wrap {
  position: relative;
  height: clamp(280px, 38dvh, 420px);
  flex: 0 0 auto;
}
.prompt-count {
  position: absolute;
  right: 10px;
  bottom: 7px;
  z-index: 2;
  color: var(--web-muted);
  font-size: 11px;
  pointer-events: none;
}
.reference-section header {
  margin-bottom: 9px;
  display: flex;
  justify-content: space-between;
}
.reference-section header small {
  color: var(--web-muted);
  font-size: 11px;
}
.reference-grid {
  display: grid;
  grid-template-columns: repeat(4, minmax(0, 1fr));
  gap: 7px;
}
.reference-item,
.reference-add {
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 8px;
}
.reference-item {
  cursor: pointer;
}
.reference-item img {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.reference-item > button {
  width: 28px;
  height: 28px;
  position: absolute;
  top: 5px;
  right: 5px;
  display: grid;
  place-items: center;
  color: var(--web-text);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 6px;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity 0.18s,
    background-color 0.18s,
    border-color 0.18s;
}
.reference-item:hover > button,
.reference-item:focus-within > button {
  opacity: 1;
}
.reference-add {
  min-height: 76px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 5px;
  color: var(--web-muted);
  border-style: dashed;
  cursor: pointer;
  font-size: 11px;
  transition:
    color 0.18s,
    background-color 0.18s,
    border-color 0.18s;
}
.reference-add:hover:not(:disabled) {
  color: var(--web-text);
  background: var(--web-hover);
  border-color: var(--web-line-emphasis);
}
.reference-add:disabled {
  cursor: not-allowed;
  opacity: 0.6;
}
.generation-parameters {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.generation-parameters__grid {
  display: grid;
  grid-template-columns: repeat(2, minmax(0, 1fr));
  gap: 7px;
}
.product-settings__footer {
  margin-top: auto;
  padding: 14px 18px;
  border-top: 1px solid var(--web-line-strong);
}
.product-settings__footer :deep(.el-button) {
  width: 100%;
  min-height: 44px;
}
.generation-button__content,
.generation-button__action,
.generation-button__price {
  display: inline-flex;
  align-items: center;
}
.generation-button__content {
  justify-content: center;
  gap: 14px;
  white-space: nowrap;
}
.generation-button__action {
  gap: 6px;
}
.generation-button__action strong {
  font-size: 15px;
  font-weight: 650;
  line-height: 1;
}
.generation-button__price {
  gap: 3px;
  font-size: 12px;
  font-weight: 500;
  font-variant-numeric: tabular-nums;
  line-height: 1;
}
.generation-button__price b {
  font-size: 13px;
  font-weight: 650;
}
.product-results {
  min-height: 620px;
  display: flex;
  flex-direction: column;
}
.product-results__body {
  min-height: 0;
  padding: 18px;
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(150px, 180px));
  align-content: start;
  gap: 10px;
  overflow-y: auto;
}
.result-card {
  min-width: 0;
  position: relative;
  aspect-ratio: 1;
  overflow: hidden;
  color: var(--web-text);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 10px;
}
.result-card__preview {
  width: 100%;
  height: 100%;
  padding: 0;
  display: block;
  overflow: hidden;
  color: inherit;
  background: transparent;
  border: 0;
  cursor: pointer;
  text-align: left;
}
.result-card__preview:disabled {
  cursor: default;
}
.result-card__preview :deep(.loading-media),
.result-card__preview :deep(.loading-media-element) {
  width: 100%;
  height: 100%;
  object-fit: cover;
}
.result-card__meta {
  position: absolute;
  right: 0;
  bottom: 0;
  left: 0;
  padding: 22px 10px 9px;
  display: flex;
  flex-direction: column;
  gap: 3px;
  color: var(--web-message-text);
  background: var(--web-media-overlay);
  pointer-events: none;
}
.result-card__meta strong {
  overflow: hidden;
  font-size: 11px;
  text-overflow: ellipsis;
  white-space: nowrap;
}
.result-card__meta time {
  color: var(--web-message-muted);
  font-size: 10px;
}
.result-card__state {
  height: 100%;
  padding: 16px 16px 72px;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 9px;
  color: var(--web-muted);
  font-size: 12px;
  text-align: center;
}
.result-card__state small {
  max-width: 100%;
  overflow: hidden;
  display: -webkit-box;
  line-height: 1.45;
  overflow-wrap: anywhere;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
}
.result-card__delete {
  width: 34px;
  height: 34px;
  position: absolute;
  top: 7px;
  right: 7px;
  z-index: 2;
  display: grid;
  place-items: center;
  color: var(--web-text);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 7px;
  cursor: pointer;
  opacity: 0;
  transition:
    opacity 0.18s,
    background-color 0.18s,
    border-color 0.18s;
}
.result-card:hover .result-card__delete,
.result-card:focus-within .result-card__delete {
  opacity: 1;
}
.result-card__delete:hover {
  color: var(--web-danger);
  background: var(--web-danger-soft);
  border-color: var(--web-danger);
}
.product-results__empty {
  min-height: 300px;
  grid-column: 1 / -1;
  display: flex;
  align-items: center;
  justify-content: center;
  flex-direction: column;
  gap: 9px;
  color: var(--web-muted);
  font-size: 12px;
  text-align: center;
}
.product-results__empty strong {
  color: var(--web-text-secondary);
}
.product-results__empty p {
  margin: 0;
}
.is-spinning {
  animation: product-spin 0.9s linear infinite;
}
@keyframes product-spin {
  to {
    transform: rotate(360deg);
  }
}
.image-preview {
  position: fixed;
  inset: 0;
  z-index: 3000;
  display: grid;
  place-items: center;
  overflow: hidden;
  color: var(--web-text-strong);
  background: var(--web-dialog-mask);
}
.image-preview__topbar {
  height: 64px;
  position: absolute;
  top: 0;
  right: 0;
  left: 0;
  padding: 0 18px;
  display: flex;
  align-items: center;
  gap: 8px;
}
.image-preview__topbar span {
  color: var(--web-muted);
  font-size: 12px;
}
.image-preview__topbar button,
.image-preview__arrow,
.image-preview__actions button,
.image-preview__prompt button {
  min-width: 44px;
  min-height: 44px;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 7px;
  color: var(--web-text);
  background: var(--web-surface);
  border: 1px solid var(--web-line-strong);
  border-radius: 8px;
  cursor: pointer;
  transition:
    color 0.18s,
    background-color 0.18s,
    border-color 0.18s;
}
.image-preview__topbar button {
  margin-left: auto;
}
.image-preview__image {
  width: calc(100vw - 160px);
  height: calc(100dvh - 150px);
}
.image-preview__arrow {
  position: absolute;
  top: 50%;
  z-index: 1;
}
.image-preview__arrow--previous {
  left: 18px;
}
.image-preview__arrow--next {
  right: 18px;
  transform: rotate(180deg);
}
.image-preview__actions {
  position: absolute;
  bottom: 12px;
  left: 50%;
  padding: 4px;
  display: flex;
  gap: 4px;
  background: var(--web-surface-soft);
  border: 1px solid var(--web-line-strong);
  border-radius: 10px;
  transform: translateX(-50%);
}
.image-preview__actions button {
  min-width: 36px;
  min-height: 36px;
  padding: 0 12px;
  font-size: 13px;
}
.image-preview__prompt {
  width: min(420px, calc(100vw - 32px));
  max-height: calc(100dvh - 130px);
  position: absolute;
  top: 82px;
  right: 18px;
  padding: 18px;
  overflow-y: auto;
  color: var(--web-text);
  background: var(--web-surface-soft);
  border: 1px solid var(--web-line-strong);
  border-radius: 12px;
}
.image-preview__prompt header {
  padding-bottom: 14px;
  display: flex;
  align-items: center;
  justify-content: space-between;
  border-bottom: 1px solid var(--web-line-strong);
}
.image-preview__prompt button {
  min-height: 38px;
  padding: 0 12px;
}
.image-preview__prompt p {
  margin: 16px 0 0;
  color: var(--web-text-secondary);
  line-height: 1.75;
  white-space: pre-wrap;
}
@media (max-width: 1050px) {
  .product-workbench {
    grid-template-columns: 320px minmax(0, 1fr);
  }
  .product-results__body {
    grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
  }
}
@media (max-width: 820px) {
  .product-workbench-shell {
    height: auto;
    min-height: 100dvh;
    overflow: hidden;
  }
  .product-workbench-shell :deep(.creative-shell__scroll) {
    padding-bottom: 56px;
    display: block;
    overflow-y: auto;
  }
  .product-workbench {
    grid-template-columns: 1fr;
    flex: none;
  }
  .product-settings,
  .product-results {
    min-height: 0;
  }
  .product-results__body {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .image-preview__image {
    width: calc(100vw - 32px);
    height: calc(100dvh - 170px);
  }
  .image-preview__arrow {
    top: auto;
    bottom: 82px;
  }
  .image-preview__prompt {
    top: auto;
    right: 16px;
    bottom: 82px;
    left: 16px;
    width: auto;
    max-height: 42dvh;
  }
}
@media (max-width: 480px) {
  .generation-parameters__grid {
    grid-template-columns: 1fr;
  }
  .product-results__body {
    grid-template-columns: repeat(2, minmax(0, 1fr));
  }
  .result-card {
    aspect-ratio: 4 / 3;
  }
}
</style>
