feat: 图像拓展功能优化和防重复点击

- 图像拓展API改为使用URL方式,避免Base64长度限制(61440字节)
- 先将图片上传到S3获取公网URL,再调用Dashscope API
- 添加防重复点击功能,处理中时禁用所有图片处理按钮
- 添加自动滚动功能,点击处理按钮后自动滚动到最新消息
- 处理完成后也会自动滚动到底部显示结果
This commit is contained in:
Your Name 2025-12-29 16:18:46 +08:00
parent 7526a0159c
commit 1f213bb92d
6 changed files with 217 additions and 16 deletions

View File

@ -85,26 +85,61 @@ async function uploadBase64ToS3(base64Data: string, mimeType: string = "image/jp
export const maxDuration = 180; export const maxDuration = 180;
/** /**
* 图像拓展核心实现 * 将图片上传到S3并返回公网URL(用于API调用)
* @param imageBase64 - Base64编码的图片数据 */
async function uploadImageToS3ForAPI(imageBase64: string, mimeType: string = "image/jpeg"): Promise<string> {
try {
// 解析 base64 数据
const buffer = Buffer.from(imageBase64, "base64");
// 生成文件名
const ext = mimeType.split("/")[1] || "jpg";
const timestamp = Date.now();
const randomStr = Math.random().toString(36).slice(2, 8);
const filename = `extend-input-${timestamp}-${randomStr}.${ext}`;
console.log(`[Extend API] 上传原图到S3,大小: ${(buffer.length / 1024).toFixed(2)}KB`);
// 转换为 ArrayBuffer
const arrayBuffer = buffer.buffer.slice(
buffer.byteOffset,
buffer.byteOffset + buffer.byteLength
) as ArrayBuffer;
// 上传到 S3
const result = await uploadFile(filename, arrayBuffer);
console.log(`[Extend API] ✅ 原图已上传到S3: ${result.url}`);
return result.url;
} catch (error) {
console.error("[Extend API] 上传到S3失败:", error);
throw new Error(`图片上传到存储桶失败: ${error instanceof Error ? error.message : "未知错误"}。请检查系统设置中的存储配置。`);
}
}
/**
* 图像拓展核心实现(使用URL方式,避免Base64长度限制)
* @param imageUrl - 图片的公网URL
* @param apiKey - Dashscope API Key * @param apiKey - Dashscope API Key
* @param xScale - X轴放大倍数(默认1.5) * @param xScale - X轴放大倍数(默认1.5)
* @param yScale - Y轴放大倍数(默认1.5) * @param yScale - Y轴放大倍数(默认1.5)
* @returns 拓展后的图片URL * @returns 拓展后的图片URL
*/ */
async function extendImage( async function extendImage(
imageBase64: string, imageUrl: string,
apiKey: string, apiKey: string,
xScale: number = 1.5, xScale: number = 1.5,
yScale: number = 1.5 yScale: number = 1.5
): Promise<string> { ): Promise<string> {
console.log("[Extend API] 开始图像拓展..."); console.log("[Extend API] 开始图像拓展...");
console.log("[Extend API] 使用图片URL:", imageUrl);
// 1. 提交拓展任务 // 1. 提交拓展任务(使用URL方式,避免Base64长度限制)
const requestBody = { const requestBody = {
model: "image-out-painting", model: "image-out-painting",
input: { input: {
image_base64: imageBase64, image_url: imageUrl,
}, },
parameters: { parameters: {
x_scale: xScale, x_scale: xScale,
@ -187,6 +222,7 @@ async function extendImage(
throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`); throw new Error(`图像拓展任务超时(等待${maxAttempts * 2 + 3}秒)`);
} }
/** /**
* 图像拓展API路由 * 图像拓展API路由
* 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理 * 从数据库读取图像拓展API配置,调用Dashscope API进行图像拓展处理
@ -286,14 +322,19 @@ export async function POST(request: NextRequest) {
console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`); console.log(`[Extend API] 图片Base64长度: ${imageBase64.length} 字符`);
// 3. 调用图像拓展API // 3. 先将图片上传到S3获取公网URL(使用URL方式避免Base64长度限制)
console.log("[Extend API] 上传原图到S3获取公网URL...");
const inputImageUrl = await uploadImageToS3ForAPI(imageBase64, "image/jpeg");
console.log("[Extend API] 原图URL:", inputImageUrl);
// 4. 调用图像拓展API(使用URL方式)
const startTime = Date.now(); const startTime = Date.now();
const xScale = config.xScale || 1.5; const xScale = config.xScale || 1.5;
const yScale = config.yScale || 1.5; const yScale = config.yScale || 1.5;
console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`); console.log(`[Extend API] 拓展参数: X=${xScale}, Y=${yScale}`);
const extendedImageUrl = await extendImage(imageBase64, config.apiKey, xScale, yScale); const extendedImageUrl = await extendImage(inputImageUrl, config.apiKey, xScale, yScale);
console.log("[Extend API] 拓展后图片URL:", extendedImageUrl); console.log("[Extend API] 拓展后图片URL:", extendedImageUrl);
// 4. 下载拓展后的图片 // 4. 下载拓展后的图片
@ -350,3 +391,4 @@ export async function POST(request: NextRequest) {
} }
} }

View File

@ -111,6 +111,10 @@ export function Chat({
const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext); const [usage, setUsage] = useState<AppUsage | undefined>(initialLastContext);
const [showCreditCardAlert, setShowCreditCardAlert] = useState(false); const [showCreditCardAlert, setShowCreditCardAlert] = useState(false);
const [currentModelId, setCurrentModelId] = useState(initialChatModel); const [currentModelId, setCurrentModelId] = useState(initialChatModel);
// 图片处理状态,防止重复点击
const [isProcessingEnhance, setIsProcessingEnhance] = useState(false);
const [isProcessingRmbg, setIsProcessingRmbg] = useState(false);
const [isProcessingExtend, setIsProcessingExtend] = useState(false);
const currentModelIdRef = useRef(currentModelId); const currentModelIdRef = useRef(currentModelId);
// 🆕 视频参数状态 // 🆕 视频参数状态
@ -502,6 +506,13 @@ export function Chat({
// 处理图片变清晰 // 处理图片变清晰
const handleImageEnhance = async (imageUrl: string) => { const handleImageEnhance = async (imageUrl: string) => {
// 防止重复点击
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
toast.info("正在处理中,请稍候...");
return;
}
setIsProcessingEnhance(true);
const messageId = generateUUID(); const messageId = generateUUID();
// 创建临时加载消息 // 创建临时加载消息
@ -514,6 +525,17 @@ export function Chat({
setMessages((prev) => [...prev, loadingMessage]); setMessages((prev) => [...prev, loadingMessage]);
// 延迟滚动到底部,确保DOM已更新
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
try { try {
// 获取图片并转换为Base64 // 获取图片并转换为Base64
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
@ -562,6 +584,17 @@ export function Chat({
prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
); );
// 滚动到底部,显示处理结果
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
// 保存消息到数据库 // 保存消息到数据库
try { try {
await fetch("/api/messages", { await fetch("/api/messages", {
@ -593,12 +626,15 @@ export function Chat({
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
// 移除加载消息 // 移除加载消息
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
} finally {
setIsProcessingEnhance(false);
} }
}; };
reader.onerror = () => { reader.onerror = () => {
toast.error("图片读取失败"); toast.error("图片读取失败");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingEnhance(false);
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
@ -606,11 +642,19 @@ export function Chat({
console.error("[变清晰] 错误:", error); console.error("[变清晰] 错误:", error);
toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingEnhance(false);
} }
}; };
// 处理图片抠图 // 处理图片抠图
const handleImageRmbg = async (imageUrl: string) => { const handleImageRmbg = async (imageUrl: string) => {
// 防止重复点击
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
toast.info("正在处理中,请稍候...");
return;
}
setIsProcessingRmbg(true);
const messageId = generateUUID(); const messageId = generateUUID();
// 创建临时加载消息 // 创建临时加载消息
@ -623,6 +667,17 @@ export function Chat({
setMessages((prev) => [...prev, loadingMessage]); setMessages((prev) => [...prev, loadingMessage]);
// 延迟滚动到底部,确保DOM已更新
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
try { try {
// 获取图片并转换为Base64 // 获取图片并转换为Base64
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
@ -671,6 +726,17 @@ export function Chat({
prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
); );
// 滚动到底部,显示处理结果
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
// 保存消息到数据库 // 保存消息到数据库
try { try {
await fetch("/api/messages", { await fetch("/api/messages", {
@ -702,12 +768,15 @@ export function Chat({
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
// 移除加载消息 // 移除加载消息
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
} finally {
setIsProcessingRmbg(false);
} }
}; };
reader.onerror = () => { reader.onerror = () => {
toast.error("图片读取失败"); toast.error("图片读取失败");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingRmbg(false);
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
@ -715,11 +784,19 @@ export function Chat({
console.error("[抠图] 错误:", error); console.error("[抠图] 错误:", error);
toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingRmbg(false);
} }
}; };
// 处理图像拓展 // 处理图像拓展
const handleImageExtend = async (imageUrl: string) => { const handleImageExtend = async (imageUrl: string) => {
// 防止重复点击
if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) {
toast.info("正在处理中,请稍候...");
return;
}
setIsProcessingExtend(true);
const messageId = generateUUID(); const messageId = generateUUID();
// 创建临时加载消息 // 创建临时加载消息
@ -734,6 +811,17 @@ export function Chat({
setMessages((prev) => [...prev, loadingMessage]); setMessages((prev) => [...prev, loadingMessage]);
// 延迟滚动到底部,确保DOM已更新
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
try { try {
// 获取图片并转换为Base64 // 获取图片并转换为Base64
const response = await fetch(imageUrl); const response = await fetch(imageUrl);
@ -782,6 +870,17 @@ export function Chat({
prev.map((msg) => (msg.id === messageId ? finalMessage : msg)) prev.map((msg) => (msg.id === messageId ? finalMessage : msg))
); );
// 滚动到底部,显示处理结果
setTimeout(() => {
const messagesContainer = document.querySelector('[role="log"]');
if (messagesContainer) {
messagesContainer.scrollTo({
top: messagesContainer.scrollHeight,
behavior: "smooth",
});
}
}, 100);
// 保存消息到数据库 // 保存消息到数据库
try { try {
await fetch("/api/messages", { await fetch("/api/messages", {
@ -813,12 +912,15 @@ export function Chat({
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
// 移除加载消息 // 移除加载消息
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
} finally {
setIsProcessingExtend(false);
} }
}; };
reader.onerror = () => { reader.onerror = () => {
toast.error("图片读取失败"); toast.error("图片读取失败");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingExtend(false);
}; };
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
@ -826,6 +928,7 @@ export function Chat({
console.error("[图像拓展] 错误:", error); console.error("[图像拓展] 错误:", error);
toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试");
setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setMessages((prev) => prev.filter((msg) => msg.id !== messageId));
setIsProcessingExtend(false);
} }
}; };
@ -861,6 +964,9 @@ export function Chat({
onImageEnhance={handleImageEnhance} onImageEnhance={handleImageEnhance}
onImageRmbg={handleImageRmbg} onImageRmbg={handleImageRmbg}
onImageExtend={handleImageExtend} onImageExtend={handleImageExtend}
isProcessingEnhance={isProcessingEnhance}
isProcessingRmbg={isProcessingRmbg}
isProcessingExtend={isProcessingExtend}
pendingGeneration={pendingGeneration} pendingGeneration={pendingGeneration}
imageCount={imageParams?.imageCount || 1} imageCount={imageParams?.imageCount || 1}
/> />

View File

@ -12,10 +12,13 @@ type ResponseProps = ComponentProps<typeof Streamdown> & {
onImageEnhance?: (imageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void;
onImageRmbg?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void;
onImageExtend?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void;
isProcessingEnhance?: boolean;
isProcessingRmbg?: boolean;
isProcessingExtend?: boolean;
}; };
export const Response = memo( export const Response = memo(
({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, ...props }: ResponseProps) => ( ({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, onImageExtend, isProcessingEnhance, isProcessingRmbg, isProcessingExtend, ...props }: ResponseProps) => (
<Streamdown <Streamdown
className={cn( 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", "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",
@ -34,6 +37,9 @@ export const Response = memo(
onImageEnhance={onImageEnhance} onImageEnhance={onImageEnhance}
onImageRmbg={onImageRmbg} onImageRmbg={onImageRmbg}
onImageExtend={onImageExtend} onImageExtend={onImageExtend}
isProcessingEnhance={isProcessingEnhance}
isProcessingRmbg={isProcessingRmbg}
isProcessingExtend={isProcessingExtend}
/>; />;
}, },
video: ({ src, alt }: { src?: string; alt?: string }) => { video: ({ src, alt }: { src?: string; alt?: string }) => {
@ -52,7 +58,10 @@ export const Response = memo(
prevProps.onDrawingComplete === nextProps.onDrawingComplete && prevProps.onDrawingComplete === nextProps.onDrawingComplete &&
prevProps.onImageEnhance === nextProps.onImageEnhance && prevProps.onImageEnhance === nextProps.onImageEnhance &&
prevProps.onImageRmbg === nextProps.onImageRmbg && prevProps.onImageRmbg === nextProps.onImageRmbg &&
prevProps.onImageExtend === nextProps.onImageExtend prevProps.onImageExtend === nextProps.onImageExtend &&
prevProps.isProcessingEnhance === nextProps.isProcessingEnhance &&
prevProps.isProcessingRmbg === nextProps.isProcessingRmbg &&
prevProps.isProcessingExtend === nextProps.isProcessingExtend
); );
Response.displayName = "Response"; Response.displayName = "Response";

View File

@ -14,6 +14,9 @@ export function ImagePreview({
onImageEnhance, onImageEnhance,
onImageRmbg, onImageRmbg,
onImageExtend, onImageExtend,
isProcessingEnhance,
isProcessingRmbg,
isProcessingExtend,
imageIndex imageIndex
}: { }: {
src: string; src: string;
@ -28,6 +31,9 @@ export function ImagePreview({
onImageEnhance?: (imageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void;
onImageRmbg?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void;
onImageExtend?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void;
isProcessingEnhance?: boolean;
isProcessingRmbg?: boolean;
isProcessingExtend?: boolean;
imageIndex?: number; imageIndex?: number;
}) { }) {
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
@ -226,11 +232,14 @@ export function ImagePreview({
<div className="absolute top-2 left-2 flex gap-1.5 z-10"> <div className="absolute top-2 left-2 flex gap-1.5 z-10">
{onImageEnhance && ( {onImageEnhance && (
<button <button
className="flex items-center gap-1.5 rounded-full bg-blue-600/90 hover:bg-blue-700 px-2.5 py-1.5 text-white transition-colors shadow-lg" className="flex items-center gap-1.5 rounded-full bg-blue-600/90 hover:bg-blue-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onImageEnhance(original); if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
onImageEnhance(original);
}
}} }}
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
type="button" type="button"
aria-label="变清晰" aria-label="变清晰"
title="变清晰" title="变清晰"
@ -241,11 +250,14 @@ export function ImagePreview({
)} )}
{onImageRmbg && ( {onImageRmbg && (
<button <button
className="flex items-center gap-1.5 rounded-full bg-purple-600/90 hover:bg-purple-700 px-2.5 py-1.5 text-white transition-colors shadow-lg" className="flex items-center gap-1.5 rounded-full bg-purple-600/90 hover:bg-purple-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onImageRmbg(original); if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
onImageRmbg(original);
}
}} }}
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
type="button" type="button"
aria-label="抠图" aria-label="抠图"
title="抠图" title="抠图"
@ -256,11 +268,14 @@ export function ImagePreview({
)} )}
{onImageExtend && ( {onImageExtend && (
<button <button
className="flex items-center gap-1.5 rounded-full bg-orange-600/90 hover:bg-orange-700 px-2.5 py-1.5 text-white transition-colors shadow-lg" className="flex items-center gap-1.5 rounded-full bg-orange-600/90 hover:bg-orange-700 px-2.5 py-1.5 text-white transition-colors shadow-lg disabled:opacity-50 disabled:cursor-not-allowed"
onClick={(e) => { onClick={(e) => {
e.stopPropagation(); e.stopPropagation();
onImageExtend(original); if (!isProcessingEnhance && !isProcessingRmbg && !isProcessingExtend) {
onImageExtend(original);
}
}} }}
disabled={isProcessingEnhance || isProcessingRmbg || isProcessingExtend}
type="button" type="button"
aria-label="图像拓展" aria-label="图像拓展"
title="图像拓展" title="图像拓展"

View File

@ -151,6 +151,9 @@ const PurePreviewMessage = ({
onImageEnhance, onImageEnhance,
onImageRmbg, onImageRmbg,
onImageExtend, onImageExtend,
isProcessingEnhance,
isProcessingRmbg,
isProcessingExtend,
}: { }: {
chatId: string; chatId: string;
message: ChatMessage; message: ChatMessage;
@ -166,6 +169,9 @@ const PurePreviewMessage = ({
onImageEnhance?: (imageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void;
onImageRmbg?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void;
onImageExtend?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void;
isProcessingEnhance?: boolean;
isProcessingRmbg?: boolean;
isProcessingExtend?: boolean;
}) => { }) => {
const [mode, setMode] = useState<"view" | "edit">("view"); const [mode, setMode] = useState<"view" | "edit">("view");
@ -299,6 +305,9 @@ const PurePreviewMessage = ({
onImageEnhance={onImageEnhance} onImageEnhance={onImageEnhance}
onImageRmbg={onImageRmbg} onImageRmbg={onImageRmbg}
onImageExtend={onImageExtend} onImageExtend={onImageExtend}
isProcessingEnhance={isProcessingEnhance}
isProcessingRmbg={isProcessingRmbg}
isProcessingExtend={isProcessingExtend}
> >
{sanitizeText(part.content)} {sanitizeText(part.content)}
</Response> </Response>

View File

@ -40,6 +40,9 @@ type MessagesProps = {
onImageEnhance?: (imageUrl: string) => void; onImageEnhance?: (imageUrl: string) => void;
onImageRmbg?: (imageUrl: string) => void; onImageRmbg?: (imageUrl: string) => void;
onImageExtend?: (imageUrl: string) => void; onImageExtend?: (imageUrl: string) => void;
isProcessingEnhance?: boolean;
isProcessingRmbg?: boolean;
isProcessingExtend?: boolean;
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态 pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
imageCount?: number; // 🆕 用户选择的图片数量 imageCount?: number; // 🆕 用户选择的图片数量
}; };
@ -59,6 +62,9 @@ function PureMessages({
onImageEnhance, onImageEnhance,
onImageRmbg, onImageRmbg,
onImageExtend, onImageExtend,
isProcessingEnhance = false,
isProcessingRmbg = false,
isProcessingExtend = false,
pendingGeneration, pendingGeneration,
imageCount = 1, imageCount = 1,
}: MessagesProps) { }: MessagesProps) {
@ -176,7 +182,7 @@ function PureMessages({
} }
}, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel]); }, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel]);
// 🆕 监听新消息添加,当用户发送带图片的消息时,强制滚动到底部 // 🆕 监听新消息添加,当用户发送带图片的消息或添加加载消息时,强制滚动到底部
const prevMessagesLengthRef = useRef(messages.length); const prevMessagesLengthRef = useRef(messages.length);
useEffect(() => { useEffect(() => {
if (messages.length > prevMessagesLengthRef.current) { if (messages.length > prevMessagesLengthRef.current) {
@ -191,6 +197,17 @@ function PureMessages({
setTimeout(() => scrollToBottom("smooth"), 100); setTimeout(() => scrollToBottom("smooth"), 100);
} }
} }
// 如果是新添加的助手消息且是加载消息(正在变清晰/抠图/图像拓展),强制滚动到底部
if (lastMessage?.role === "assistant") {
const firstPart = lastMessage.parts?.[0];
if (firstPart && "text" in firstPart) {
const loadingText = firstPart.text;
if (loadingText === "正在变清晰..." || loadingText === "正在抠图..." || loadingText === "正在图像拓展...") {
// 延迟一小段时间确保DOM已更新
setTimeout(() => scrollToBottom("smooth"), 100);
}
}
}
} }
prevMessagesLengthRef.current = messages.length; prevMessagesLengthRef.current = messages.length;
}, [messages, scrollToBottom]); }, [messages, scrollToBottom]);
@ -229,6 +246,9 @@ function PureMessages({
onImageEnhance={onImageEnhance} onImageEnhance={onImageEnhance}
onImageRmbg={onImageRmbg} onImageRmbg={onImageRmbg}
onImageExtend={onImageExtend} onImageExtend={onImageExtend}
isProcessingEnhance={isProcessingEnhance}
isProcessingRmbg={isProcessingRmbg}
isProcessingExtend={isProcessingExtend}
/> />
))} ))}