138 lines
4.8 KiB
TypeScript
138 lines
4.8 KiB
TypeScript
"use client";
|
||
|
||
import { memo } from "react";
|
||
import { ChevronDown, Grid2x2, CircleDot } from "lucide-react";
|
||
import { cn } from "@/lib/utils";
|
||
|
||
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;
|
||
}
|
||
|
||
const PROVIDER_MODEL_LABEL: Record<string, string> = {
|
||
"alibaba-image": "千问图片",
|
||
"ruxa-image": "RUXA 图片",
|
||
"fal-image": "FAL 图片",
|
||
"openai-image": "OpenAI 图片",
|
||
midjourney: "Midjourney",
|
||
"volcano-vision": "即梦 4.5",
|
||
gemini: "Gemini 图片",
|
||
krea: "Krea 图片",
|
||
};
|
||
|
||
const PHASE_HINT: Record<NonNullable<ImageGenerationLoadingProps["phase"]>, string> = {
|
||
idle: "任务已创建,正在初始化生成参数。",
|
||
queued: "任务已进入队列,请稍候。",
|
||
processing: "模型正在生成图片,请稍候。",
|
||
finalizing: "图片已生成完成,正在同步到对话。",
|
||
failed: "生成失败,请重试。",
|
||
};
|
||
|
||
export const ImageGenerationLoading = memo(
|
||
({
|
||
imageCount = 1,
|
||
maxImages = 6,
|
||
modelName,
|
||
providerType,
|
||
phase = "processing",
|
||
elapsedSeconds = 0,
|
||
}: ImageGenerationLoadingProps) => {
|
||
const count = Math.min(Math.max(imageCount, 1), maxImages);
|
||
const resolvedModelName =
|
||
modelName?.trim() || PROVIDER_MODEL_LABEL[providerType || ""] || "AI 图片模型";
|
||
const estimateRangeLabel = "60~120秒";
|
||
const elapsedLabel =
|
||
elapsedSeconds > 0 ? `已等待 ${elapsedSeconds}s` : `预计 ${estimateRangeLabel}`;
|
||
const isFailed = phase === "failed";
|
||
|
||
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-[560px] 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>
|
||
|
||
<div
|
||
className={cn(
|
||
"relative overflow-hidden rounded-[22px] border border-[#e1e4ea] bg-[#eaedf2]",
|
||
count === 1 ? "h-[420px] w-full max-w-[340px]" : "grid gap-3 p-3",
|
||
count > 1 && "grid-cols-2"
|
||
)}
|
||
>
|
||
{count === 1 ? (
|
||
<>
|
||
<div className="absolute inset-0 bg-gradient-to-r from-[#e9ecf1] via-[#dfe4eb] to-[#e9ecf1] opacity-90" />
|
||
<div className="generation-skeleton-shimmer absolute inset-0 bg-[linear-gradient(105deg,transparent_0%,rgba(255,255,255,0.45)_48%,transparent_100%)]" />
|
||
</>
|
||
) : (
|
||
Array.from({ length: count }).map((_, index) => (
|
||
<div
|
||
key={index}
|
||
className="relative h-[180px] overflow-hidden rounded-2xl border border-[#dfe3e9] bg-[#e7ebf0]"
|
||
>
|
||
<div className="absolute inset-0 bg-gradient-to-r from-[#e7ebf0] via-[#dce2ea] to-[#e7ebf0]" />
|
||
<div
|
||
className="generation-skeleton-shimmer absolute inset-0 bg-[linear-gradient(105deg,transparent_0%,rgba(255,255,255,0.42)_48%,transparent_100%)]"
|
||
style={{ animationDelay: `${index * 0.2}s` }}
|
||
/>
|
||
</div>
|
||
))
|
||
)}
|
||
</div>
|
||
|
||
<div className="flex items-center gap-2 pt-1 text-[13px] text-[#9aa3b2]">
|
||
<Grid2x2 className="h-5 w-5" />
|
||
<span className="font-medium">{isFailed ? "生成失败" : "设计图处理中"}</span>
|
||
</div>
|
||
|
||
<div className="space-y-0.5 text-[#9aa3b2]">
|
||
<div className="text-[13px] font-medium">
|
||
使用 {resolvedModelName} 生成图片,预计 {estimateRangeLabel}
|
||
</div>
|
||
{(phase !== "processing" || elapsedSeconds > 0) && (
|
||
<div className="text-[13px]">
|
||
{PHASE_HINT[phase]} {elapsedLabel}
|
||
</div>
|
||
)}
|
||
</div>
|
||
</div>
|
||
|
||
<style jsx>{`
|
||
.generation-skeleton-shimmer {
|
||
transform: translateX(-100%);
|
||
animation: generation-shimmer 1.8s ease-in-out infinite;
|
||
}
|
||
|
||
@keyframes generation-shimmer {
|
||
0% {
|
||
transform: translateX(-110%);
|
||
}
|
||
100% {
|
||
transform: translateX(110%);
|
||
}
|
||
}
|
||
`}</style>
|
||
</div>
|
||
);
|
||
}
|
||
);
|
||
|
||
ImageGenerationLoading.displayName = "ImageGenerationLoading";
|