feat: 在聊天对话中为图片添加变清晰和抠图功能
- 在图片预览组件左上角添加变清晰和抠图按钮 - 创建图片处理加载组件(类似Gemini生图的动态卡片效果) - 实现点击按钮后的处理逻辑:创建临时消息、调用API、更新消息 - 添加消息保存API用于保存处理后的图片消息 - 当消息文本为'正在变清晰...'或'正在抠图...'时显示动态加载组件 - 处理完成后替换加载卡片为处理后的图片并保存到数据库
This commit is contained in:
parent
5e0b0d41db
commit
1c7bd9e2d0
55
app/api/messages/route.ts
Normal file
55
app/api/messages/route.ts
Normal file
@ -0,0 +1,55 @@
|
||||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { auth } from "@/app/(auth)/auth";
|
||||
import { saveMessages } from "@/lib/db/queries";
|
||||
import type { DBMessage } from "@/lib/db/schema";
|
||||
|
||||
/**
|
||||
* 保存消息到数据库的API
|
||||
* 用于图片处理等场景中保存助手回复消息
|
||||
*/
|
||||
export async function POST(request: NextRequest) {
|
||||
try {
|
||||
const session = await auth();
|
||||
|
||||
if (!session?.user?.id) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json();
|
||||
const { chatId, message } = body;
|
||||
|
||||
if (!chatId || !message) {
|
||||
return NextResponse.json(
|
||||
{ error: "Missing required fields: chatId and message" },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
// 构造DBMessage对象
|
||||
const dbMessage: DBMessage = {
|
||||
id: message.id,
|
||||
chatId,
|
||||
role: message.role || "assistant",
|
||||
parts: message.parts || [],
|
||||
attachments: message.attachments || [],
|
||||
createdAt: message.createdAt ? new Date(message.createdAt) : new Date(),
|
||||
videoStorageStatus: null,
|
||||
temporaryVideoUrl: null,
|
||||
permanentVideoUrl: null,
|
||||
archivedAt: null,
|
||||
videoMetadata: null,
|
||||
};
|
||||
|
||||
// 保存消息到数据库
|
||||
await saveMessages({ messages: [dbMessage] });
|
||||
|
||||
return NextResponse.json({ success: true });
|
||||
} catch (error) {
|
||||
console.error("[Messages API] Error:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to save message" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@ -32,7 +32,7 @@ import { useDataStream } from "@/components/data-stream-provider";
|
||||
import { Messages } from "@/components/messages";
|
||||
import { MultimodalInput } from "@/components/multimodal-input";
|
||||
import { getChatHistoryPaginationKey } from "@/components/sidebar-history";
|
||||
import { toast } from "@/components/toast";
|
||||
import { toast } from "sonner";
|
||||
import type { VisibilityType } from "@/components/visibility-selector";
|
||||
import type { VideoGenerationParams } from "@/components/video-params-bar";
|
||||
import type { ImageGenerationParams } from "@/components/image-params-bar";
|
||||
@ -317,10 +317,7 @@ export function Chat({
|
||||
) {
|
||||
setShowCreditCardAlert(true);
|
||||
} else {
|
||||
toast({
|
||||
type: "error",
|
||||
description: error.message,
|
||||
});
|
||||
toast.error(error.message);
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -448,6 +445,214 @@ export function Chat({
|
||||
setInput((prev) => prev ? prev : "请根据我的标注修改这张图片:");
|
||||
};
|
||||
|
||||
// 处理图片变清晰
|
||||
const handleImageEnhance = async (imageUrl: string) => {
|
||||
const messageId = generateUUID();
|
||||
|
||||
// 创建临时加载消息
|
||||
const loadingMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: "正在变清晰..." }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
setMessages((prev) => [...prev, loadingMessage]);
|
||||
|
||||
try {
|
||||
// 获取图片并转换为Base64
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64Data = (reader.result as string).split(",")[1];
|
||||
|
||||
// 调用变清晰API
|
||||
const enhanceResponse = await fetch("/api/enhance", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image_base64: base64Data,
|
||||
prompt: "图片变清晰处理",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!enhanceResponse.ok) {
|
||||
const error = await enhanceResponse.json();
|
||||
throw new Error(error.error || "变清晰处理失败");
|
||||
}
|
||||
|
||||
const result = await enhanceResponse.json();
|
||||
|
||||
if (result.success && result.original_url) {
|
||||
// 构造图片Markdown(使用与Gemini生成相同的格式)
|
||||
const imageMarkdown = ``;
|
||||
|
||||
// 创建最终消息
|
||||
const finalMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: imageMarkdown }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
// 更新消息
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
||||
);
|
||||
|
||||
// 保存消息到数据库
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chatId: id,
|
||||
message: finalMessage,
|
||||
}),
|
||||
});
|
||||
} catch (saveError) {
|
||||
console.error("[Chat] Failed to save message:", saveError);
|
||||
}
|
||||
|
||||
toast.success(`变清晰完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
|
||||
} else {
|
||||
throw new Error(result.error || "变清晰处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[变清晰] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
toast.error("图片读取失败");
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
} catch (error) {
|
||||
console.error("[变清晰] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
}
|
||||
};
|
||||
|
||||
// 处理图片抠图
|
||||
const handleImageRmbg = async (imageUrl: string) => {
|
||||
const messageId = generateUUID();
|
||||
|
||||
// 创建临时加载消息
|
||||
const loadingMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: "正在抠图..." }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
setMessages((prev) => [...prev, loadingMessage]);
|
||||
|
||||
try {
|
||||
// 获取图片并转换为Base64
|
||||
const response = await fetch(imageUrl);
|
||||
const blob = await response.blob();
|
||||
const reader = new FileReader();
|
||||
|
||||
reader.onloadend = async () => {
|
||||
try {
|
||||
const base64Data = (reader.result as string).split(",")[1];
|
||||
|
||||
// 调用抠图API
|
||||
const rmbgResponse = await fetch("/api/rmbg", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
image_base64: base64Data,
|
||||
prompt: "抠图处理",
|
||||
}),
|
||||
});
|
||||
|
||||
if (!rmbgResponse.ok) {
|
||||
const error = await rmbgResponse.json();
|
||||
throw new Error(error.error || "抠图处理失败");
|
||||
}
|
||||
|
||||
const result = await rmbgResponse.json();
|
||||
|
||||
if (result.success && result.original_url) {
|
||||
// 构造图片Markdown(使用与Gemini生成相同的格式)
|
||||
const imageMarkdown = ``;
|
||||
|
||||
// 创建最终消息
|
||||
const finalMessage: ChatMessage = {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: imageMarkdown }],
|
||||
createdAt: new Date().toISOString(),
|
||||
} as ChatMessage;
|
||||
|
||||
// 更新消息
|
||||
setMessages((prev) =>
|
||||
prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
|
||||
);
|
||||
|
||||
// 保存消息到数据库
|
||||
try {
|
||||
await fetch("/api/messages", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
chatId: id,
|
||||
message: {
|
||||
id: messageId,
|
||||
role: "assistant",
|
||||
parts: [{ type: "text", text: imageMarkdown }],
|
||||
attachments: [],
|
||||
createdAt: new Date().toISOString(),
|
||||
},
|
||||
}),
|
||||
});
|
||||
} catch (saveError) {
|
||||
console.error("[Chat] Failed to save message:", saveError);
|
||||
}
|
||||
|
||||
toast.success(`抠图完成!处理时间: ${result.processing_time?.toFixed(2) || "未知"}秒`);
|
||||
} else {
|
||||
throw new Error(result.error || "抠图处理失败");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("[抠图] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||
// 移除加载消息
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
}
|
||||
};
|
||||
|
||||
reader.onerror = () => {
|
||||
toast.error("图片读取失败");
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
};
|
||||
|
||||
reader.readAsDataURL(blob);
|
||||
} catch (error) {
|
||||
console.error("[抠图] 错误:", error);
|
||||
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
|
||||
setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
|
||||
}
|
||||
};
|
||||
|
||||
useAutoResume({
|
||||
autoResume,
|
||||
initialMessages,
|
||||
@ -477,6 +682,8 @@ export function Chat({
|
||||
votes={votes}
|
||||
onImageEdit={handleImageEdit}
|
||||
onDrawingComplete={handleDrawingComplete}
|
||||
onImageEnhance={handleImageEnhance}
|
||||
onImageRmbg={handleImageRmbg}
|
||||
pendingGeneration={pendingGeneration}
|
||||
imageCount={imageParams?.imageCount || 1}
|
||||
/>
|
||||
|
||||
@ -9,10 +9,12 @@ import { VideoPreview } from "@/components/video-preview";
|
||||
type ResponseProps = ComponentProps<typeof Streamdown> & {
|
||||
onImageEdit?: (imageUrl: string) => void;
|
||||
onDrawingComplete?: (annotatedImageUrl: string) => void;
|
||||
onImageEnhance?: (imageUrl: string) => void;
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
};
|
||||
|
||||
export const Response = memo(
|
||||
({ className, onImageEdit, onDrawingComplete, ...props }: ResponseProps) => (
|
||||
({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, ...props }: ResponseProps) => (
|
||||
<Streamdown
|
||||
className={cn(
|
||||
"size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto",
|
||||
@ -23,7 +25,14 @@ export const Response = memo(
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
return <ImagePreview src={src} alt={alt} onImageEdit={onImageEdit} onDrawingComplete={onDrawingComplete} />;
|
||||
return <ImagePreview
|
||||
src={src}
|
||||
alt={alt}
|
||||
onImageEdit={onImageEdit}
|
||||
onDrawingComplete={onDrawingComplete}
|
||||
onImageEnhance={onImageEnhance}
|
||||
onImageRmbg={onImageRmbg}
|
||||
/>;
|
||||
},
|
||||
video: ({ src, alt }: { src?: string; alt?: string }) => {
|
||||
if (!src) {
|
||||
@ -36,7 +45,11 @@ export const Response = memo(
|
||||
/>
|
||||
),
|
||||
(prevProps: ResponseProps, nextProps: ResponseProps) =>
|
||||
prevProps.children === nextProps.children && prevProps.onImageEdit === nextProps.onImageEdit && prevProps.onDrawingComplete === nextProps.onDrawingComplete
|
||||
prevProps.children === nextProps.children &&
|
||||
prevProps.onImageEdit === nextProps.onImageEdit &&
|
||||
prevProps.onDrawingComplete === nextProps.onDrawingComplete &&
|
||||
prevProps.onImageEnhance === nextProps.onImageEnhance &&
|
||||
prevProps.onImageRmbg === nextProps.onImageRmbg
|
||||
);
|
||||
|
||||
Response.displayName = "Response";
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import React, { useState } from "react";
|
||||
import { X, Download, Edit, Target, Paintbrush } from "lucide-react";
|
||||
import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { ImageAnnotator } from "./image-annotator";
|
||||
|
||||
@ -11,6 +11,8 @@ export function ImagePreview({
|
||||
onImageEdit,
|
||||
onAnnotate,
|
||||
onDrawingComplete,
|
||||
onImageEnhance,
|
||||
onImageRmbg,
|
||||
imageIndex
|
||||
}: {
|
||||
src: string;
|
||||
@ -22,6 +24,8 @@ export function ImagePreview({
|
||||
formattedText: string;
|
||||
}) => void;
|
||||
onDrawingComplete?: (annotatedImageUrl: string) => void;
|
||||
onImageEnhance?: (imageUrl: string) => void;
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
imageIndex?: number;
|
||||
}) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
@ -199,6 +203,40 @@ export function ImagePreview({
|
||||
onClick={() => setIsOpen(true)}
|
||||
/>
|
||||
|
||||
{/* 左上角:变清晰和抠图按钮 */}
|
||||
{(onImageEnhance || onImageRmbg) && (
|
||||
<div className="absolute top-2 left-2 flex gap-1.5 opacity-0 transition-opacity group-hover:opacity-100 z-10">
|
||||
{onImageEnhance && (
|
||||
<button
|
||||
className="rounded-full bg-blue-600/90 hover:bg-blue-700 p-2 text-white transition-colors shadow-lg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onImageEnhance(original);
|
||||
}}
|
||||
type="button"
|
||||
aria-label="变清晰"
|
||||
title="变清晰"
|
||||
>
|
||||
<Sparkles size={16} />
|
||||
</button>
|
||||
)}
|
||||
{onImageRmbg && (
|
||||
<button
|
||||
className="rounded-full bg-green-600/90 hover:bg-green-700 p-2 text-white transition-colors shadow-lg"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
onImageRmbg(original);
|
||||
}}
|
||||
type="button"
|
||||
aria-label="抠图"
|
||||
title="抠图"
|
||||
>
|
||||
<Scissors size={16} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 悬停时显示的操作按钮 */}
|
||||
<div className="absolute bottom-2 right-2 flex gap-1.5 opacity-0 transition-opacity group-hover:opacity-100">
|
||||
{/* 画图按钮 */}
|
||||
|
||||
158
components/image-processing-loading.tsx
Normal file
158
components/image-processing-loading.tsx
Normal file
@ -0,0 +1,158 @@
|
||||
"use client";
|
||||
|
||||
import { memo } from "react";
|
||||
import { SparklesIcon } from "./icons";
|
||||
import { Scissors } from "lucide-react";
|
||||
|
||||
interface ImageProcessingLoadingProps {
|
||||
type: "enhance" | "rmbg"; // 处理类型:变清晰或抠图
|
||||
}
|
||||
|
||||
/**
|
||||
* 图片处理加载占位符组件
|
||||
* 显示类似 Gemini 生图的动态卡片效果
|
||||
*/
|
||||
export const ImageProcessingLoading = memo(({ type }: ImageProcessingLoadingProps) => {
|
||||
const isEnhance = type === "enhance";
|
||||
const color = isEnhance ? "blue" : "green";
|
||||
const text = isEnhance ? "正在变清晰" : "正在抠图";
|
||||
const icon = isEnhance ? SparklesIcon : Scissors;
|
||||
const Icon = icon;
|
||||
|
||||
// 根据类型设置颜色类名
|
||||
const colorClasses = isEnhance
|
||||
? {
|
||||
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",
|
||||
}
|
||||
: {
|
||||
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",
|
||||
};
|
||||
|
||||
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";
|
||||
|
||||
@ -23,6 +23,7 @@ import { MessageEditor } from "./message-editor";
|
||||
import { MessageReasoning } from "./message-reasoning";
|
||||
import { PreviewAttachment } from "./preview-attachment";
|
||||
import { Weather } from "./weather";
|
||||
import { ImageProcessingLoading } from "./image-processing-loading";
|
||||
|
||||
/**
|
||||
* 解析文本中的标注内容,返回文本片段数组
|
||||
@ -147,6 +148,8 @@ const PurePreviewMessage = ({
|
||||
requiresScrollPadding: _requiresScrollPadding,
|
||||
onImageEdit,
|
||||
onDrawingComplete,
|
||||
onImageEnhance,
|
||||
onImageRmbg,
|
||||
}: {
|
||||
chatId: string;
|
||||
message: ChatMessage;
|
||||
@ -159,6 +162,8 @@ const PurePreviewMessage = ({
|
||||
requiresScrollPadding: boolean;
|
||||
onImageEdit?: (imageUrl: string) => void;
|
||||
onDrawingComplete?: (annotatedImageUrl: string) => void;
|
||||
onImageEnhance?: (imageUrl: string) => void;
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
}) => {
|
||||
const [mode, setMode] = useState<"view" | "edit">("view");
|
||||
|
||||
@ -235,6 +240,17 @@ const PurePreviewMessage = ({
|
||||
|
||||
if (type === "text") {
|
||||
if (mode === "view") {
|
||||
// 检查是否是图片处理的加载消息
|
||||
if (part.text === "正在变清晰..." || part.text === "正在抠图...") {
|
||||
const isEnhance = part.text === "正在变清晰...";
|
||||
return (
|
||||
<ImageProcessingLoading
|
||||
key={key}
|
||||
type={isEnhance ? "enhance" : "rmbg"}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// 应用视频内容过滤
|
||||
const filteredText = filterVideoContent(part.text);
|
||||
const textParts = parseAnnotations(filteredText);
|
||||
@ -267,7 +283,13 @@ const PurePreviewMessage = ({
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Response key={partIndex} onImageEdit={onImageEdit} onDrawingComplete={onDrawingComplete}>
|
||||
<Response
|
||||
key={partIndex}
|
||||
onImageEdit={onImageEdit}
|
||||
onDrawingComplete={onDrawingComplete}
|
||||
onImageEnhance={onImageEnhance}
|
||||
onImageRmbg={onImageRmbg}
|
||||
>
|
||||
{sanitizeText(part.content)}
|
||||
</Response>
|
||||
);
|
||||
|
||||
@ -37,6 +37,8 @@ type MessagesProps = {
|
||||
selectedModelId: string;
|
||||
onImageEdit?: (imageUrl: string) => void;
|
||||
onDrawingComplete?: (annotatedImageUrl: string) => void;
|
||||
onImageEnhance?: (imageUrl: string) => void;
|
||||
onImageRmbg?: (imageUrl: string) => void;
|
||||
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
||||
imageCount?: number; // 🆕 用户选择的图片数量
|
||||
};
|
||||
@ -53,6 +55,8 @@ function PureMessages({
|
||||
selectedModelId,
|
||||
onImageEdit,
|
||||
onDrawingComplete,
|
||||
onImageEnhance,
|
||||
onImageRmbg,
|
||||
pendingGeneration,
|
||||
imageCount = 1,
|
||||
}: MessagesProps) {
|
||||
@ -220,6 +224,8 @@ function PureMessages({
|
||||
}
|
||||
onImageEdit={onImageEdit}
|
||||
onDrawingComplete={onDrawingComplete}
|
||||
onImageEnhance={onImageEnhance}
|
||||
onImageRmbg={onImageRmbg}
|
||||
/>
|
||||
))}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user