ai-chatbot/components/image-generation-loading.tsx
Your Name de955db82f
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: polish canvas chat UI and loading states
2026-02-21 01:43:22 +08:00

150 lines
5.4 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, 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 图片",
"volcano-vision": "即梦 4.5",
gemini: "Gemini 图片",
};
const PHASE_TITLE: Record<NonNullable<ImageGenerationLoadingProps["phase"]>, string> = {
idle: "准备中",
queued: "排队中",
processing: "生成中",
finalizing: "加载结果",
failed: "生成失败",
};
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 estimateSeconds = Math.max(45, Math.min(180, count * 60));
const estimateMinutes = Math.max(1, Math.ceil(estimateSeconds / 60));
const elapsedLabel =
elapsedSeconds > 0 ? `已等待 ${elapsedSeconds}s` : `预计 ${estimateSeconds}s`;
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-[760px] space-y-3">
<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="text-[16px] leading-8 text-[#3a4352]">
现在开始生成设计图,使用 {resolvedModelName} 模型,预计需要 {estimateMinutes} 分钟。
</div>
<div className="flex items-center gap-2 text-[#a8b1bf]">
<CircleDot className="h-3.5 w-3.5 fill-current" />
<span className="text-[16px]">{resolvedModelName}</span>
</div>
<div className="text-[42px] font-semibold text-[#37404d]">{PHASE_TITLE[phase]}</div>
<div
className={cn(
"relative overflow-hidden rounded-[22px] border border-[#e1e4ea] bg-[#eaedf2]",
count === 1 ? "h-[620px] w-full max-w-[680px]" : "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-[220px] 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-[18px] text-[#8c95a3]">
<Grid2x2 className="h-5 w-5" />
<span className="font-medium">{isFailed ? "生成失败" : "设计图生成中"}</span>
</div>
<div className="space-y-0.5 text-[#a0a8b6]">
<div className="text-[15px] font-medium">
使用 {resolvedModelName} 生成图片,预计 {estimateSeconds}
</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";