lovart3/components/image-generation-loading.tsx

262 lines
9.3 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 { SparklesIcon } from "./icons";
// 预定义的颜色主题
const COLOR_THEMES = {
purple: {
ping: "bg-purple-500/20",
pulse: "bg-purple-500/10",
ring: "ring-purple-500/30",
text: "text-purple-500",
dot: "bg-purple-500",
gradientFrom: "from-purple-400",
gradientVia: "via-pink-400",
gradientTo: "to-blue-400",
overlay1From: "from-purple-500/40",
overlay1To: "to-pink-500/40",
overlay2From: "from-blue-500/30",
overlay2To: "to-purple-500/30",
},
blue: {
ping: "bg-blue-500/20",
pulse: "bg-blue-500/10",
ring: "ring-blue-500/30",
text: "text-blue-500",
dot: "bg-blue-500",
gradientFrom: "from-blue-400",
gradientVia: "via-cyan-400",
gradientTo: "to-teal-400",
overlay1From: "from-blue-500/40",
overlay1To: "to-cyan-500/40",
overlay2From: "from-teal-500/30",
overlay2To: "to-blue-500/30",
},
green: {
ping: "bg-green-500/20",
pulse: "bg-green-500/10",
ring: "ring-green-500/30",
text: "text-green-500",
dot: "bg-green-500",
gradientFrom: "from-green-400",
gradientVia: "via-emerald-400",
gradientTo: "to-teal-400",
overlay1From: "from-green-500/40",
overlay1To: "to-emerald-500/40",
overlay2From: "from-teal-500/30",
overlay2To: "to-green-500/30",
},
orange: {
ping: "bg-orange-500/20",
pulse: "bg-orange-500/10",
ring: "ring-orange-500/30",
text: "text-orange-500",
dot: "bg-orange-500",
gradientFrom: "from-orange-400",
gradientVia: "via-amber-400",
gradientTo: "to-yellow-400",
overlay1From: "from-orange-500/40",
overlay1To: "to-amber-500/40",
overlay2From: "from-yellow-500/30",
overlay2To: "to-orange-500/30",
},
red: {
ping: "bg-red-500/20",
pulse: "bg-red-500/10",
ring: "ring-red-500/30",
text: "text-red-500",
dot: "bg-red-500",
gradientFrom: "from-red-400",
gradientVia: "via-rose-400",
gradientTo: "to-pink-400",
overlay1From: "from-red-500/40",
overlay1To: "to-rose-500/40",
overlay2From: "from-pink-500/30",
overlay2To: "to-red-500/30",
},
} as const;
type ColorTheme = keyof typeof COLOR_THEMES;
interface ImageGenerationLoadingProps {
imageCount?: number; // 生成图片数量,默认 1
maxImages?: number; // 最大图片数限制,默认 6
colorTheme?: ColorTheme; // 颜色主题,默认 purple
modelName?: string; // 模型名称,用于显示
providerType?: string; // 提供商类型,用于自动选择颜色
}
/**
* 通用图片生成加载占位符组件
* 支持配置颜色主题和图片数量
*/
export const ImageGenerationLoading = memo(({
imageCount = 1,
maxImages = 6,
colorTheme,
modelName,
providerType,
}: ImageGenerationLoadingProps) => {
// 根据 providerType 自动选择颜色主题
const getAutoColorTheme = (): ColorTheme => {
if (colorTheme) return colorTheme;
switch (providerType) {
case "gemini":
return "purple";
case "alibaba-image":
return "orange";
case "ruxa-image":
return "green";
case "volcano-vision":
return "blue";
default:
return "purple";
}
};
const theme = COLOR_THEMES[getAutoColorTheme()];
// 确保数量在有效范围内
const count = Math.min(Math.max(imageCount, 1), maxImages);
// 获取显示名称
const displayName = modelName || (providerType === "alibaba-image" ? "千问图片编辑" :
providerType === "ruxa-image" ? "RUXA 图片编辑" :
providerType === "volcano-vision" ? "即梦4.5" : "AI");
return (
<div
className="group/message fade-in w-full animate-in duration-300"
data-role="assistant"
data-testid="message-assistant-image-loading"
>
<div className="flex items-start justify-start gap-3">
{/* 头像 */}
<div className="relative -mt-1 flex size-8 shrink-0 items-center justify-center">
<div className={`absolute inset-0 rounded-full ${theme.ping} animate-ping`} />
<div className={`absolute inset-0 rounded-full ${theme.pulse} animate-pulse`} />
<div className={`relative flex size-8 items-center justify-center rounded-full bg-background ring-2 ${theme.ring}`}>
<div className={`animate-spin ${theme.text}`}>
<SparklesIcon size={14} />
</div>
</div>
</div>
<div className="flex flex-col gap-3">
{/* 文本提示 */}
<div className="flex items-center gap-1.5">
<span className="text-base font-medium text-foreground">
正在生成 {count} 张图片
</span>
<span className="flex gap-1">
<span
className={`h-2 w-2 rounded-full ${theme.dot} animate-bounce`}
style={{ animationDelay: "0ms" }}
/>
<span
className={`h-2 w-2 rounded-full ${theme.dot} animate-bounce`}
style={{ animationDelay: "150ms" }}
/>
<span
className={`h-2 w-2 rounded-full ${theme.dot} animate-bounce`}
style={{ animationDelay: "300ms" }}
/>
</span>
</div>
{/* 图片占位符 - 横向排列,每个 170x200px */}
<div className="flex gap-3 flex-wrap">
{Array.from({ length: count }, (_, index) => index).map((index) => (
<div
key={index}
className="relative overflow-hidden rounded-xl"
style={{ width: "170px", height: "200px" }}
>
{/* 底层渐变背景 */}
<div className={`absolute inset-0 bg-gradient-to-br ${theme.gradientFrom} ${theme.gradientVia} ${theme.gradientTo} opacity-60`} />
{/* 动态模糊叠加层 1 */}
<div
className={`absolute inset-0 bg-gradient-to-tr ${theme.overlay1From} via-transparent ${theme.overlay1To} animate-pulse`}
style={{ animationDuration: "2s", animationDelay: `${index * 0.3}s` }}
/>
{/* 动态模糊叠加层 2 */}
<div
className={`absolute inset-0 bg-gradient-to-bl ${theme.overlay2From} via-transparent ${theme.overlay2To} animate-pulse`}
style={{ animationDuration: "2.5s", animationDelay: `${0.5 + index * 0.3}s` }}
/>
{/* 噪点纹理效果 */}
<div
className="absolute inset-0 opacity-30"
style={{
backgroundImage: `url("data:image/svg+xml,%3Csvg viewBox='0 0 400 400' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.9' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E")`,
}}
/>
{/* 模糊叠加 */}
<div className="absolute inset-0 backdrop-blur-sm" />
{/* 中心图标 */}
<div className="absolute inset-0 flex items-center justify-center">
<div className="relative">
{/* 外圈光晕 */}
<div className="absolute -inset-4 rounded-full bg-white/20 animate-ping" style={{ animationDuration: "1.5s", animationDelay: `${index * 0.2}s` }} />
{/* 图片图标 */}
<svg
className="h-10 w-10 text-white/80 animate-pulse"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
style={{ animationDuration: "1.5s", animationDelay: `${index * 0.2}s` }}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
/>
</svg>
</div>
</div>
{/* 流光效果 */}
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full animate-shimmer"
style={{
animation: "shimmer 2s infinite",
animationDelay: `${index * 0.4}s`,
}}
/>
</div>
))}
</div>
{/* 提示文字 */}
<span className="text-xs text-muted-foreground">
{displayName} 正在创作 {count} 张图片,请稍候...
</span>
</div>
</div>
{/* 流光动画样式 */}
<style jsx>{`
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
`}</style>
</div>
);
});
ImageGenerationLoading.displayName = "ImageGenerationLoading";
export default ImageGenerationLoading;