"use client"; import { memo, useEffect, useState } from "react"; import { ChevronDown, CircleDot } from "lucide-react"; import { GenerationDotPlaceholder } from "@/components/generation-dot-placeholder"; type ColorTheme = "purple" | "blue" | "green" | "orange" | "red"; interface ImageGenerationLoadingProps { imageCount?: number; maxImages?: number; colorTheme?: ColorTheme; modelName?: string; providerType?: string; phase?: "idle" | "queued" | "processing" | "finalizing" | "failed"; elapsedSeconds?: number; queuePosition?: number; sameModelRunningCount?: number; concurrencyLimit?: number; estimatedWaitMs?: number; } const PROVIDER_MODEL_LABEL: Record = { "alibaba-image": "千问图片", "ruxa-image": "RUXA 图片", "fal-image": "FAL 图片", "snapgen-image": "SnapGen 图片", "openai-image": "OpenAI 图片", bananaimage: "BananaImage 图片", visionary: "Visionary 图片", atlascloud: "Atlas Cloud 图片", vip98k: "VIP98K 图片", "stepfun-image": "StepFun 图片", midjourney: "Midjourney", "volcano-vision": "即梦 4.5", gemini: "Gemini 图片", krea: "Krea 图片", }; const PHASE_HINT: Record, string> = { idle: "任务已创建,正在初始化生成参数。", queued: "任务已进入队列,请稍候。", processing: "模型正在生成图片,请稍候。", finalizing: "图片已返回,正在保存并同步到对话。", failed: "本次处理已停止,请查看对话中的原因和建议。", }; const LONG_WAIT_SECONDS = 5 * 60; function formatImageQueueWaitLabel(estimatedWaitMs?: number) { const numeric = Number(estimatedWaitMs || 0); if (!Number.isFinite(numeric) || numeric <= 0) { return "预计即将开始"; } const seconds = Math.max(1, Math.round(numeric / 1000)); if (seconds < 60) { return `预计 ${seconds} 秒`; } const minutes = Math.floor(seconds / 60); const remainSeconds = seconds % 60; return remainSeconds > 0 ? `预计 ${minutes} 分 ${remainSeconds} 秒` : `预计 ${minutes} 分钟`; } export const ImageGenerationLoading = memo( ({ imageCount = 1, maxImages = 6, modelName, providerType, phase = "processing", elapsedSeconds, queuePosition, sameModelRunningCount, concurrencyLimit, estimatedWaitMs, }: ImageGenerationLoadingProps) => { const shouldTrackElapsed = phase === "queued" || phase === "processing" || phase === "finalizing"; const [internalElapsedSeconds, setInternalElapsedSeconds] = useState(0); useEffect(() => { if (typeof elapsedSeconds === "number") return; if (!shouldTrackElapsed) { setInternalElapsedSeconds(0); return; } const startedAt = Date.now(); setInternalElapsedSeconds(0); const timer = window.setInterval(() => { setInternalElapsedSeconds(Math.floor((Date.now() - startedAt) / 1000)); }, 1000); return () => window.clearInterval(timer); }, [elapsedSeconds, modelName, phase, providerType, shouldTrackElapsed]); const count = Math.min(Math.max(imageCount, 1), maxImages); const resolvedModelName = modelName?.trim() || PROVIDER_MODEL_LABEL[providerType || ""] || "AI 图片模型"; const resolvedElapsedSeconds = typeof elapsedSeconds === "number" ? elapsedSeconds : internalElapsedSeconds; const isLongWait = resolvedElapsedSeconds >= LONG_WAIT_SECONDS && (phase === "processing" || phase === "finalizing"); const elapsedLabel = resolvedElapsedSeconds > 0 ? `已等待 ${resolvedElapsedSeconds}s` : ""; const phaseHint = PHASE_HINT[phase]; const queueLine = phase === "queued" && Number.isFinite(queuePosition) ? Math.max(0, Number(queuePosition || 0)) > 0 ? `排队中,前方 ${Math.max(0, Number(queuePosition || 0))} 个` : "排队中,等待进入执行" : null; const queueDetailLine = phase === "queued" ? `当前模型运行 ${Math.max( 0, Number(sameModelRunningCount || 0) )}/${Math.max(1, Number(concurrencyLimit || 0) || 1)},${formatImageQueueWaitLabel( estimatedWaitMs )}` : null; return (
开始生成
{resolvedModelName}
1} phase={phase} title={isLongWait && phase === "processing" ? "模型尚未返回结果,请勿重复提交" : undefined} />
使用 {resolvedModelName} 生成图片
{queueLine ?
{queueLine}
: null} {queueDetailLine ?
{queueDetailLine}
: null} {(phase !== "processing" || resolvedElapsedSeconds > 0) && (
{phaseHint} {elapsedLabel}
)}
); } ); ImageGenerationLoading.displayName = "ImageGenerationLoading";