ai-chatbot/components/image-generation-loading.tsx

136 lines
4.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"use client";
import { memo } 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<string, string> = {
"alibaba-image": "千问图片",
"ruxa-image": "RUXA 图片",
"fal-image": "FAL 图片",
"openai-image": "OpenAI 图片",
"stepfun-image": "StepFun 图片",
midjourney: "Midjourney",
"volcano-vision": "即梦 4.5",
gemini: "Gemini 图片",
krea: "Krea 图片",
};
const PHASE_HINT: Record<NonNullable<ImageGenerationLoadingProps["phase"]>, string> = {
idle: "任务已创建,正在初始化生成参数。",
queued: "任务已进入队列,请稍候。",
processing: "模型正在生成图片,请稍候。",
finalizing: "图片已生成完成,正在同步到对话。",
failed: "生成失败,请重试。",
};
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 = 0,
queuePosition,
sameModelRunningCount,
concurrencyLimit,
estimatedWaitMs,
}: ImageGenerationLoadingProps) => {
const count = Math.min(Math.max(imageCount, 1), maxImages);
const resolvedModelName =
modelName?.trim() || PROVIDER_MODEL_LABEL[providerType || ""] || "AI 图片模型";
const estimateRangeLabel = providerType === "grsai-gpt-image" ? "2~5分钟" : "60~120秒";
const elapsedLabel =
elapsedSeconds > 0 ? `已等待 ${elapsedSeconds}s` : `预计 ${estimateRangeLabel}`;
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 (
<div
className="group/message fade-in w-full animate-in duration-300"
data-role="assistant"
data-testid="message-assistant-image-loading"
>
<div className="w-full max-w-[520px] space-y-2.5">
<div className="flex items-center gap-2 text-[13px] text-[#9aa3b2]">
<ChevronDown className="h-4 w-4" />
<span className="font-medium">开始生成</span>
</div>
<div className="flex items-center gap-2 text-[#a8b1bf]">
<CircleDot className="h-3.5 w-3.5 fill-current" />
<span className="text-[14px]">{resolvedModelName}</span>
</div>
<GenerationDotPlaceholder
className={count === 1 ? "h-[420px] w-full max-w-[420px]" : "h-[360px] w-full"}
compact={count > 1}
phase={phase}
/>
<div className="space-y-0.5 text-[#9aa3b2]">
<div className="text-[13px] font-medium">
使用 {resolvedModelName} 生成图片,预计 {estimateRangeLabel}
</div>
{queueLine ? <div className="text-[13px]">{queueLine}</div> : null}
{queueDetailLine ? <div className="text-[13px]">{queueDetailLine}</div> : null}
{(phase !== "processing" || elapsedSeconds > 0) && (
<div className="text-[13px]">
{PHASE_HINT[phase]} {elapsedLabel}
</div>
)}
</div>
</div>
</div>
);
}
);
ImageGenerationLoading.displayName = "ImageGenerationLoading";