ai-chatbot/components/image-processing-loading.tsx
Your Name 7c253e04a0
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: 添加图像拓展功能
- 新增 /api/extend 路由,集成阿里云Dashscope图像拓展API
- 在系统设置'本地AI设置'中添加图像拓展API配置(API密钥、X/Y轴放大倍数)
- 在图片标注组件中添加'图像拓展'按钮(橙色,Expand图标)
- 在聊天对话图片上添加'图像拓展'按钮,支持动态占位卡片
- 更新ImageProcessingLoading组件支持extend类型(橙色主题)
- 处理后的图片上传到S3,保存到数据库(modelId: extend-1.0)
- 同步到我的相册和出图记录
- 添加完整的API调用文档
2025-12-29 10:54:32 +08:00

181 lines
6.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";
import { Scissors, Expand } from "lucide-react";
interface ImageProcessingLoadingProps {
type: "enhance" | "rmbg" | "extend"; // 处理类型:变清晰、抠图或图像拓展
}
/**
* 图片处理加载占位符组件
* 显示类似 Gemini 生图的动态卡片效果
*/
export const ImageProcessingLoading = memo(({ type }: ImageProcessingLoadingProps) => {
const isEnhance = type === "enhance";
const isRmbg = type === "rmbg";
const isExtend = type === "extend";
let text: string;
let icon: any;
let colorClasses: any;
if (isEnhance) {
text = "正在变清晰";
icon = SparklesIcon;
colorClasses = {
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",
gradientFromOverlay: "from-blue-500/40",
gradientToOverlay: "to-blue-500/30",
};
} else if (isRmbg) {
text = "正在抠图";
icon = Scissors;
colorClasses = {
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",
gradientFromOverlay: "from-green-500/40",
gradientToOverlay: "to-green-500/30",
};
} else {
text = "正在图像拓展";
icon = Expand;
colorClasses = {
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",
gradientFromOverlay: "from-orange-500/40",
gradientToOverlay: "to-orange-500/30",
};
}
const Icon = icon;
return (
<div
className="group/message fade-in w-full animate-in duration-300"
data-role="assistant"
data-testid="message-assistant-image-processing-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 ${colorClasses.ping} animate-ping`} />
<div className={`absolute inset-0 rounded-full ${colorClasses.pulse} animate-pulse`} />
<div className={`relative flex size-8 items-center justify-center rounded-full bg-background ring-2 ${colorClasses.ring}`}>
<div className={`animate-spin ${colorClasses.text}`}>
<Icon 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">
{text}
</span>
<span className="flex gap-1">
<span
className={`h-2 w-2 rounded-full ${colorClasses.dot} animate-bounce`}
style={{ animationDelay: "0ms" }}
/>
<span
className={`h-2 w-2 rounded-full ${colorClasses.dot} animate-bounce`}
style={{ animationDelay: "150ms" }}
/>
<span
className={`h-2 w-2 rounded-full ${colorClasses.dot} animate-bounce`}
style={{ animationDelay: "300ms" }}
/>
</span>
</div>
{/* 图片占位符 - 170x200px */}
<div className="relative overflow-hidden rounded-xl" style={{ width: "170px", height: "200px" }}>
{/* 底层渐变背景 */}
<div className={`absolute inset-0 bg-gradient-to-br ${colorClasses.gradientFrom} via-purple-400 to-pink-400 opacity-60`} />
{/* 动态模糊叠加层 1 */}
<div
className={`absolute inset-0 bg-gradient-to-tr ${colorClasses.gradientFromOverlay} via-transparent to-purple-500/40 animate-pulse`}
style={{ animationDuration: "2s" }}
/>
{/* 动态模糊叠加层 2 */}
<div
className={`absolute inset-0 bg-gradient-to-bl from-purple-500/30 via-transparent ${colorClasses.gradientToOverlay} animate-pulse`}
style={{ animationDuration: "2.5s", animationDelay: "0.5s" }}
/>
{/* 噪点纹理效果 */}
<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" }} />
{/* 图标 */}
<Icon
className="h-10 w-10 text-white/80 animate-pulse"
style={{ animationDuration: "1.5s" }}
/>
</div>
</div>
{/* 流光效果 */}
<div
className="absolute inset-0 bg-gradient-to-r from-transparent via-white/20 to-transparent -translate-x-full"
style={{
animation: "shimmer 2s infinite",
}}
/>
</div>
{/* 提示文字 */}
<span className="text-xs text-muted-foreground">
正在处理图片,请稍候...
</span>
</div>
</div>
{/* 流光动画样式 */}
<style jsx>{`
@keyframes shimmer {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
`}</style>
</div>
);
});
ImageProcessingLoading.displayName = "ImageProcessingLoading";