729 lines
30 KiB
TypeScript
729 lines
30 KiB
TypeScript
import type { UseChatHelpers } from "@ai-sdk/react";
|
||
import equal from "fast-deep-equal";
|
||
import { ArrowDownIcon } from "lucide-react";
|
||
import { memo, useEffect, useState, useRef } from "react";
|
||
import useSWR from "swr";
|
||
import { useMessages } from "@/hooks/use-messages";
|
||
import type { PendingGeneration } from "@/hooks/use-pending-generation";
|
||
import type { Vote } from "@/lib/db/schema";
|
||
import type { ChatMessage } from "@/lib/types";
|
||
import { useDataStream } from "./data-stream-provider";
|
||
import { GeminiImageLoading } from "./gemini-image-loading";
|
||
import { QianwenVideoLoading } from "./qianwen-video-loading";
|
||
import { VolcanoVisionLoading } from "./volcano-vision-loading";
|
||
import { ImageGenerationLoading } from "./image-generation-loading";
|
||
import { Greeting } from "./greeting";
|
||
import { PreviewMessage, ThinkingMessage } from "./message";
|
||
|
||
// 模型数据类型
|
||
type DynamicModel = {
|
||
id: string;
|
||
providerType: string;
|
||
modelId: string;
|
||
displayName: string;
|
||
};
|
||
|
||
const fetcher = (url: string) => fetch(url).then((res) => res.json());
|
||
|
||
type MessagesProps = {
|
||
chatId: string;
|
||
status: UseChatHelpers<ChatMessage>["status"];
|
||
votes: Vote[] | undefined;
|
||
messages: ChatMessage[];
|
||
setMessages: UseChatHelpers<ChatMessage>["setMessages"];
|
||
regenerate: UseChatHelpers<ChatMessage>["regenerate"];
|
||
sendMessage: UseChatHelpers<ChatMessage>["sendMessage"];
|
||
isReadonly: boolean;
|
||
isArtifactVisible: boolean;
|
||
selectedModelId: string;
|
||
onImageEdit?: (imageUrl: string) => void;
|
||
onDrawingComplete?: (annotatedImageUrl: string) => void;
|
||
onImageEnhance?: (imageUrl: string) => void;
|
||
onImageRmbg?: (imageUrl: string) => void;
|
||
onImageExtend?: (imageUrl: string) => void;
|
||
onImageAngleEdit?: (imageUrl: string) => void;
|
||
onImageWatermarkRemove?: (imageUrl: string) => void;
|
||
isProcessingEnhance?: boolean;
|
||
isProcessingRmbg?: boolean;
|
||
isProcessingExtend?: boolean;
|
||
isProcessingAngleEdit?: boolean;
|
||
isProcessingWatermarkRemove?: boolean;
|
||
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
|
||
imageCount?: number; // 🆕 用户选择的图片数量
|
||
isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件)
|
||
imageGenerationPhase?: "idle" | "queued" | "processing" | "finalizing" | "failed";
|
||
imageGenerationStartedAt?: number | null;
|
||
};
|
||
|
||
function PureMessages({
|
||
chatId,
|
||
status,
|
||
votes,
|
||
messages,
|
||
setMessages,
|
||
regenerate,
|
||
sendMessage,
|
||
isReadonly,
|
||
selectedModelId,
|
||
onImageEdit,
|
||
onDrawingComplete,
|
||
onImageEnhance,
|
||
onImageRmbg,
|
||
onImageExtend,
|
||
onImageAngleEdit,
|
||
onImageWatermarkRemove,
|
||
isProcessingEnhance = false,
|
||
isProcessingRmbg = false,
|
||
isProcessingExtend = false,
|
||
isProcessingAngleEdit = false,
|
||
isProcessingWatermarkRemove = false,
|
||
pendingGeneration,
|
||
imageCount = 1,
|
||
isGeneratingImageFlag = false,
|
||
imageGenerationPhase = "idle",
|
||
imageGenerationStartedAt = null,
|
||
}: MessagesProps) {
|
||
const {
|
||
containerRef: messagesContainerRef,
|
||
endRef: messagesEndRef,
|
||
isAtBottom,
|
||
scrollToBottom,
|
||
hasSentMessage,
|
||
} = useMessages({
|
||
status,
|
||
});
|
||
|
||
useDataStream();
|
||
|
||
// 获取模型列表以判断当前模型类型
|
||
const { data: modelsData } = useSWR<{ models: DynamicModel[] }>(
|
||
"/api/models",
|
||
fetcher
|
||
);
|
||
|
||
// 判断当前选中的模型类型
|
||
// 🆕 包含 gemini 和 huanke-gemini 两种 Gemini 图片生成 provider
|
||
const isGeminiModel = modelsData?.models?.some(
|
||
(m: DynamicModel) => m.id === selectedModelId && (m.providerType === "gemini" || m.providerType === "gemini-image" || m.providerType === "huanke-gemini")
|
||
) ?? false;
|
||
|
||
const isQianwenVideoModel = modelsData?.models?.some(
|
||
(m: DynamicModel) => m.id === selectedModelId && m.providerType === "qianwen-video"
|
||
) ?? false;
|
||
|
||
const isVolcanoVisionModel = modelsData?.models?.some(
|
||
(m: DynamicModel) => m.id === selectedModelId && m.providerType === "volcano-vision"
|
||
) ?? false;
|
||
|
||
// 🆕 判断是否是阿里生图模型(或者智能自动模式默认使用阿里生图占位图)
|
||
const isAlibabaImageModel = modelsData?.models?.some(
|
||
(m: DynamicModel) => m.id === selectedModelId && (m.providerType === "alibaba-image" || m.providerType === "ruxa-image")
|
||
) ?? false;
|
||
|
||
// 🆕 获取当前模型的显示名称
|
||
const currentModelDisplayName = modelsData?.models?.find(
|
||
(m: DynamicModel) => m.id === selectedModelId
|
||
)?.displayName;
|
||
const currentModelProviderType = modelsData?.models?.find(
|
||
(m: DynamicModel) => m.id === selectedModelId
|
||
)?.providerType;
|
||
|
||
// 判断是否是视频生成模型(通过 providerType 或模型名称)
|
||
const isVideoModel = modelsData?.models?.some(
|
||
(m: DynamicModel) =>
|
||
m.id === selectedModelId &&
|
||
(m.providerType === "volcano-video" ||
|
||
m.modelId?.toLowerCase().includes("video") ||
|
||
m.modelId?.toLowerCase().includes("sora") ||
|
||
m.displayName?.includes("视频"))
|
||
) ?? false;
|
||
|
||
// 控制等待动画延迟消失(Gemini 图片模型延迟 4 秒,其他模型 6 秒)
|
||
const [showThinkingWithDelay, setShowThinkingWithDelay] = useState(false);
|
||
const [autoImagePlaceholderReady, setAutoImagePlaceholderReady] = useState(false);
|
||
const autoImagePlaceholderTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||
const [imageGenerationElapsedSeconds, setImageGenerationElapsedSeconds] = useState(0);
|
||
const isImageGenerationActive =
|
||
imageGenerationPhase === "queued" ||
|
||
imageGenerationPhase === "processing" ||
|
||
imageGenerationPhase === "finalizing";
|
||
const stripInternalMarkers = (text: string) =>
|
||
text
|
||
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
|
||
.trim();
|
||
|
||
useEffect(() => {
|
||
return () => {
|
||
if (autoImagePlaceholderTimerRef.current) {
|
||
clearTimeout(autoImagePlaceholderTimerRef.current);
|
||
autoImagePlaceholderTimerRef.current = null;
|
||
}
|
||
};
|
||
}, []);
|
||
|
||
useEffect(() => {
|
||
if (!isImageGenerationActive || !imageGenerationStartedAt) {
|
||
setImageGenerationElapsedSeconds(0);
|
||
return;
|
||
}
|
||
|
||
const updateElapsed = () => {
|
||
setImageGenerationElapsedSeconds(
|
||
Math.max(0, Math.floor((Date.now() - imageGenerationStartedAt) / 1000))
|
||
);
|
||
};
|
||
|
||
updateElapsed();
|
||
const timer = setInterval(updateElapsed, 1000);
|
||
return () => clearInterval(timer);
|
||
}, [isImageGenerationActive, imageGenerationStartedAt]);
|
||
|
||
useEffect(() => {
|
||
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
||
|
||
// 对于视频模型,检查是否有真正的视频内容(包含 <video 标签或 [视频] 标记)
|
||
const hasVideoContent = lastMessage?.parts && lastMessage.parts.some(
|
||
(part: any) => part.type === "text" && part.text && (
|
||
part.text.includes("[视频]") ||
|
||
part.text.includes("<video") ||
|
||
part.text.includes("视频生成完成")
|
||
)
|
||
);
|
||
|
||
// 调试日志
|
||
if (isVideoModel) {
|
||
const firstPart = lastMessage?.parts?.[0];
|
||
const messageText = firstPart && 'text' in firstPart ? firstPart.text?.substring(0, 100) : undefined;
|
||
console.log("[占位图调试] 状态:", {
|
||
status,
|
||
isVideoModel,
|
||
hasVideoContent,
|
||
messageRole: lastMessage?.role,
|
||
messageText
|
||
});
|
||
}
|
||
|
||
// 🆕 智能自动模式:检查用户最后一条消息是否包含图片,如果有则视为图片编辑场景
|
||
// 注意:需要找用户的最后一条消息,而不是整个对话的最后一条
|
||
const lastUserMessage = [...messages].reverse().find(m => m.role === "user");
|
||
const isAutoModeWithImage = selectedModelId === "auto" &&
|
||
lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image");
|
||
|
||
// 🆕 检查是否有"图片正在生成"的标记(表示对话消息已发送,正在等待图片生成)
|
||
const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user");
|
||
const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages;
|
||
const hasGenerationStart = messagesAfterLastUser.some(msg =>
|
||
msg.role === "assistant" && msg.parts?.some(
|
||
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
|
||
)
|
||
);
|
||
const hasGenerationEnd = messagesAfterLastUser.some(msg =>
|
||
msg.role === "assistant" && msg.parts?.some(
|
||
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
|
||
)
|
||
);
|
||
const isGeneratingImage = hasGenerationStart && !hasGenerationEnd;
|
||
|
||
// 对于图片模型,检查是否有图片内容(包含 ![thumb| 或 ![thumb||| 标记)
|
||
// 🆕 在智能自动模式下,需要检查最后一条用户消息之后的所有 assistant 消息
|
||
let hasImageContent = false;
|
||
if (isAutoModeWithImage) {
|
||
// 找到最后一条用户消息的索引
|
||
const lastUserIndex = messages.findLastIndex(m => m.role === "user");
|
||
// 检查之后所有的 assistant 消息是否包含图片
|
||
const assistantMessagesAfterUser = messages.slice(lastUserIndex + 1).filter(m => m.role === "assistant");
|
||
hasImageContent = assistantMessagesAfterUser.some(msg =>
|
||
msg.parts?.some((part: any) => part.type === "text" && part.text && (
|
||
part.text.includes("![thumb|") ||
|
||
part.text.includes("![thumb|||") ||
|
||
part.text.includes("![Generated Image")
|
||
))
|
||
);
|
||
} else {
|
||
// 普通模式:只检查最后一条消息
|
||
hasImageContent = lastMessage?.parts && lastMessage.parts.some(
|
||
(part: any) => part.type === "text" && part.text && (
|
||
part.text.includes("![thumb|") ||
|
||
part.text.includes("![thumb|||") ||
|
||
part.text.includes("![Generated Image")
|
||
)
|
||
) || false;
|
||
}
|
||
|
||
// 对于其他模型,检查是否有实际的文本内容
|
||
const hasTextContent = lastMessage?.parts && lastMessage.parts.some(
|
||
(part: any) => part.type === "text" && part.text && part.text.trim().length > 0
|
||
);
|
||
|
||
// 判断是否是图片生成模型(包括 Gemini、火山、阿里)
|
||
const isImageGenModel = isGeminiModel || isVolcanoVisionModel || isAlibabaImageModel;
|
||
|
||
// 🆕 智能自动模式的特殊处理
|
||
// - 收到 GENERATING_IMAGE 标记前:显示普通思考动画
|
||
// - 收到 GENERATING_IMAGE 标记后:显示生图占位图
|
||
// - 收到图片后:隐藏占位图
|
||
let hasContent = false;
|
||
if (isQianwenVideoModel || isVideoModel) {
|
||
hasContent = hasVideoContent ?? false;
|
||
} else if (isImageGenModel) {
|
||
hasContent = hasImageContent ?? false;
|
||
} else if (isAutoModeWithImage) {
|
||
// 智能自动模式:只有图片出现才算有内容
|
||
hasContent = hasImageContent ?? false;
|
||
} else {
|
||
hasContent = hasTextContent ?? false;
|
||
}
|
||
|
||
// 根据模型类型设置延迟时间
|
||
const delayTime = (isQianwenVideoModel || isVideoModel) ? 60000 : (isImageGenModel || isAutoModeWithImage) ? 4000 : 6000;
|
||
|
||
// 🆕 智能自动模式的占位图逻辑
|
||
if (isAutoModeWithImage) {
|
||
console.log("[Messages useEffect] 智能模式检查:", {
|
||
hasImageContent,
|
||
isGeneratingImage,
|
||
isGeneratingImageFlag,
|
||
status,
|
||
});
|
||
|
||
// 🆕 简化的占位图逻辑 - 完全由 isGeneratingImageFlag 控制
|
||
// isGeneratingImageFlag 的生命周期:
|
||
// - 初始: false
|
||
// - 收到 AI 文字回复后 3 秒: true(开始显示生图占位图)
|
||
// - 收到图片消息: false(隐藏占位图)
|
||
|
||
// 检查当前轮次是否已收到 AI 文字回复(只看最后一条用户消息之后)
|
||
const lastUserIndex = messages.findLastIndex((m) => m.role === "user");
|
||
const assistantMessagesAfterUser = lastUserIndex >= 0
|
||
? messages.slice(lastUserIndex + 1).filter((m) => m.role === "assistant")
|
||
: messages.filter((m) => m.role === "assistant");
|
||
const hasAssistantTextReply = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some((part: any) => {
|
||
if (part.type !== "text" || !part.text) return false;
|
||
return stripInternalMarkers(part.text).length > 0;
|
||
})
|
||
);
|
||
|
||
if (hasImageContent) {
|
||
// 有图片内容,隐藏占位图
|
||
console.log("[Messages useEffect] 有图片内容,隐藏占位图");
|
||
setShowThinkingWithDelay(false);
|
||
} else if (isGeneratingImageFlag) {
|
||
// 正在生成图片,显示生图占位图
|
||
console.log("[Messages useEffect] isGeneratingImageFlag=true,显示生成占位图");
|
||
setShowThinkingWithDelay(true);
|
||
} else if (hasAssistantTextReply) {
|
||
// 收到了 AI 文字回复但还没开始生图(延迟期间),隐藏占位图让用户看文字
|
||
console.log("[Messages useEffect] 已收到AI文字回复,延迟期间隐藏占位图");
|
||
setShowThinkingWithDelay(false);
|
||
} else if (status === "streaming" || status === "submitted") {
|
||
// 正在处理中(分析意图或流式传输),显示普通思考占位图
|
||
console.log("[Messages useEffect] streaming/submitted,显示思考占位图");
|
||
setShowThinkingWithDelay(true);
|
||
} else {
|
||
// ready 或其他状态:不显示占位图
|
||
console.log("[Messages useEffect] ready状态,隐藏占位图");
|
||
setShowThinkingWithDelay(false);
|
||
}
|
||
} else {
|
||
// 非智能自动模式的原有逻辑
|
||
// 🆕 调试日志
|
||
console.log("[Messages useEffect] 非智能模式:", {
|
||
status,
|
||
isImageGenModel,
|
||
isGeminiModel,
|
||
lastMessageRole: lastMessage?.role,
|
||
hasContent,
|
||
selectedModelId,
|
||
});
|
||
|
||
if (status === "submitted") {
|
||
console.log("[Messages useEffect] status=submitted,显示占位图");
|
||
setShowThinkingWithDelay(true);
|
||
} else if (status === "streaming" && lastMessage?.role === "assistant" && !hasContent) {
|
||
console.log("[Messages useEffect] streaming无内容,显示占位图");
|
||
setShowThinkingWithDelay(true);
|
||
} else if (status === "streaming" && lastMessage?.role === "assistant" && hasContent) {
|
||
console.log("[Messages useEffect] streaming有内容,延迟隐藏占位图");
|
||
const timer = setTimeout(() => {
|
||
setShowThinkingWithDelay(false);
|
||
}, delayTime);
|
||
return () => clearTimeout(timer);
|
||
} else {
|
||
console.log("[Messages useEffect] 其他状态,隐藏占位图");
|
||
setShowThinkingWithDelay(false);
|
||
}
|
||
}
|
||
}, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel, isAlibabaImageModel, selectedModelId, isGeneratingImageFlag]);
|
||
|
||
// 🆕 智能自动模式:确保文字可见后再显示生图占位图
|
||
useEffect(() => {
|
||
const isAutoMode = selectedModelId === "auto";
|
||
const lastUserIndex = messages.findLastIndex((m) => m.role === "user");
|
||
const assistantMessagesAfterUser = lastUserIndex >= 0
|
||
? messages.slice(lastUserIndex + 1).filter((m) => m.role === "assistant")
|
||
: messages.filter((m) => m.role === "assistant");
|
||
const hasAssistantTextReplyWithContent = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some((part: any) => {
|
||
if (part.type !== "text" || !part.text) return false;
|
||
return stripInternalMarkers(part.text).length > 0;
|
||
})
|
||
);
|
||
|
||
const hasImageContent = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some(
|
||
(part: any) =>
|
||
part.type === "text" &&
|
||
part.text &&
|
||
(part.text.includes("![thumb|") ||
|
||
part.text.includes("![thumb|||") ||
|
||
part.text.includes("![Generated Image"))
|
||
)
|
||
);
|
||
const hasGenerationStart = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some(
|
||
(part: any) =>
|
||
part.type === "text" &&
|
||
part.text?.includes("<!-- IMAGE_GENERATION_START -->")
|
||
)
|
||
);
|
||
const isAutoImageFlow =
|
||
isAutoMode && (isGeneratingImageFlag || hasGenerationStart || hasImageContent);
|
||
|
||
if (!isAutoImageFlow || !isGeneratingImageFlag || !hasAssistantTextReplyWithContent || hasImageContent) {
|
||
if (autoImagePlaceholderTimerRef.current) {
|
||
clearTimeout(autoImagePlaceholderTimerRef.current);
|
||
autoImagePlaceholderTimerRef.current = null;
|
||
}
|
||
if (autoImagePlaceholderReady) {
|
||
setAutoImagePlaceholderReady(false);
|
||
}
|
||
return;
|
||
}
|
||
|
||
if (!autoImagePlaceholderReady && !autoImagePlaceholderTimerRef.current) {
|
||
autoImagePlaceholderTimerRef.current = setTimeout(() => {
|
||
setAutoImagePlaceholderReady(true);
|
||
autoImagePlaceholderTimerRef.current = null;
|
||
}, 800);
|
||
}
|
||
}, [messages, selectedModelId, isGeneratingImageFlag, autoImagePlaceholderReady]);
|
||
|
||
// 🆕 监听新消息添加,当用户发送带图片的消息或添加加载消息时,强制滚动到底部
|
||
const prevMessagesLengthRef = useRef(messages.length);
|
||
useEffect(() => {
|
||
if (messages.length > prevMessagesLengthRef.current) {
|
||
const lastMessage = messages[messages.length - 1];
|
||
// 如果是新添加的用户消息且包含图片附件,强制滚动到底部
|
||
if (lastMessage?.role === "user") {
|
||
const hasImageAttachment = lastMessage.parts?.some(
|
||
(part: any) => part.type === "file" && part.mediaType?.startsWith("image/")
|
||
);
|
||
if (hasImageAttachment) {
|
||
// 延迟一小段时间确保DOM已更新
|
||
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;
|
||
}, [messages, scrollToBottom]);
|
||
|
||
// 🔍 调试:追踪消息数组变化
|
||
useEffect(() => {
|
||
console.log("[Messages] 📋 消息数组更新:", {
|
||
count: messages.length,
|
||
messages: messages.map(m => ({
|
||
id: m.id?.substring(0, 8),
|
||
role: m.role,
|
||
partsCount: m.parts?.length,
|
||
firstPartType: m.parts?.[0]?.type,
|
||
firstPartText: m.parts?.[0]?.type === "text" ? m.parts[0].text?.substring(0, 50) : undefined
|
||
}))
|
||
});
|
||
}, [messages]);
|
||
|
||
return (
|
||
<div className="relative flex-1">
|
||
<div
|
||
className="absolute inset-0 touch-pan-y overflow-y-auto"
|
||
ref={messagesContainerRef}
|
||
>
|
||
<div className="mx-auto flex min-w-0 max-w-4xl flex-col gap-4 px-2 py-4 md:gap-6 md:px-4">
|
||
{messages.length === 0 && <Greeting />}
|
||
|
||
{messages.map((message, index) => {
|
||
// 🆕 清理消息中的内部标记(如 <!-- GENERATING_IMAGE -->)
|
||
const cleanedMessage = {
|
||
...message,
|
||
parts: message.parts?.map((part: any) => {
|
||
if (part.type === "text" && part.text) {
|
||
return {
|
||
...part,
|
||
text: part.text
|
||
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
|
||
.trim()
|
||
};
|
||
}
|
||
return part;
|
||
})
|
||
};
|
||
|
||
// 🆕 检查清理后的消息是否有实际内容
|
||
// 如果所有 parts 都为空,跳过渲染
|
||
const hasContent = cleanedMessage.parts?.some((part: any) => {
|
||
if (part.type === "text") return part.text && part.text.trim().length > 0;
|
||
if (part.type === "file" || part.type === "image") return part.url;
|
||
return true; // 其他类型的 part 默认有内容
|
||
});
|
||
|
||
// 如果没有内容,不渲染这条消息
|
||
if (!hasContent && cleanedMessage.parts?.length > 0) {
|
||
return null;
|
||
}
|
||
|
||
return (
|
||
<PreviewMessage
|
||
chatId={chatId}
|
||
isLoading={
|
||
status === "streaming" && messages.length - 1 === index
|
||
}
|
||
isReadonly={isReadonly}
|
||
key={message.id}
|
||
message={cleanedMessage}
|
||
regenerate={regenerate}
|
||
sendMessage={sendMessage}
|
||
requiresScrollPadding={
|
||
hasSentMessage && index === messages.length - 1
|
||
}
|
||
setMessages={setMessages}
|
||
vote={
|
||
votes
|
||
? votes.find((vote) => vote.messageId === message.id)
|
||
: undefined
|
||
}
|
||
onImageEdit={onImageEdit}
|
||
onDrawingComplete={onDrawingComplete}
|
||
onImageEnhance={onImageEnhance}
|
||
onImageRmbg={onImageRmbg}
|
||
onImageExtend={onImageExtend}
|
||
onImageAngleEdit={onImageAngleEdit}
|
||
onImageWatermarkRemove={onImageWatermarkRemove}
|
||
isProcessingEnhance={isProcessingEnhance}
|
||
isProcessingRmbg={isProcessingRmbg}
|
||
isProcessingExtend={isProcessingExtend}
|
||
isProcessingAngleEdit={isProcessingAngleEdit}
|
||
isProcessingWatermarkRemove={isProcessingWatermarkRemove}
|
||
/>
|
||
);
|
||
})}
|
||
|
||
{/* 等待动画:根据模型类型显示不同的加载效果 */}
|
||
{/* 🆕 支持恢复的生成状态(用户离开再返回) */}
|
||
{(() => {
|
||
// 检查最后一条消息是否是用户消息(表示还在等待 AI 响应)
|
||
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
||
const isWaitingForResponse = lastMessage?.role === "user";
|
||
|
||
// 恢复的待处理状态:有 pendingGeneration 且最后一条是用户消息
|
||
// 🆕 但如果收到了 AI 回复(包含 GENERATING_IMAGE 标记),不应该再显示恢复的占位图
|
||
const hasAssistantReply = messages.some(msg =>
|
||
msg.role === "assistant" && msg.parts?.some((part: any) => {
|
||
if (part.type !== "text" || !part.text) return false;
|
||
const cleanedText = part.text
|
||
.replace(/\n*<!-- GENERATING_IMAGE -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_START -->\n*/g, "")
|
||
.replace(/\n*<!-- IMAGE_GENERATION_END -->\n*/g, "")
|
||
.trim();
|
||
return cleanedText.length > 0;
|
||
})
|
||
);
|
||
const hasRestoredPending = pendingGeneration && isWaitingForResponse && !hasAssistantReply;
|
||
|
||
// 🔍 调试日志
|
||
console.log("[Messages] 占位图状态检查:", {
|
||
pendingGeneration,
|
||
lastMessageRole: lastMessage?.role,
|
||
isWaitingForResponse,
|
||
hasRestoredPending,
|
||
hasAssistantReply,
|
||
showThinkingWithDelay,
|
||
messagesCount: messages.length,
|
||
isGeneratingImageFlag,
|
||
});
|
||
|
||
// 显示条件:分阶段图片生成状态 / 正常等待动画 / 恢复的待处理状态
|
||
const shouldShow = isImageGenerationActive || showThinkingWithDelay || hasRestoredPending;
|
||
|
||
if (!shouldShow) {
|
||
console.log("[Messages] 不显示占位图: shouldShow=false");
|
||
return null;
|
||
}
|
||
|
||
console.log("[Messages] ✅ 显示占位图!");
|
||
|
||
// 优先使用恢复的状态,否则使用当前模型类型
|
||
const modelType = pendingGeneration?.modelType;
|
||
const showGemini = modelType === "gemini" || (!modelType && isGeminiModel);
|
||
const showVolcano = modelType === "volcano-vision" || (!modelType && isVolcanoVisionModel);
|
||
const showAlibabaImage = modelType === "alibaba-image" || (!modelType && isAlibabaImageModel);
|
||
const showVideo = modelType === "volcano-video" || modelType === "qianwen-video" || (!modelType && (isQianwenVideoModel || isVideoModel));
|
||
|
||
const isAutoMode = selectedModelId === "auto";
|
||
|
||
// 🆕 检查是否收到了 GENERATING_IMAGE 标记(表示对话已发送,开始生图)
|
||
const lastUserIndexForMarkers = messages.findLastIndex(m => m.role === "user");
|
||
const messagesAfterLastUser = lastUserIndexForMarkers >= 0 ? messages.slice(lastUserIndexForMarkers + 1) : messages;
|
||
const hasGeneratingMarker = messagesAfterLastUser.some(msg =>
|
||
msg.role === "assistant" && msg.parts?.some(
|
||
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_START -->")
|
||
)
|
||
);
|
||
const hasGenerationEnd = messagesAfterLastUser.some(msg =>
|
||
msg.role === "assistant" && msg.parts?.some(
|
||
(part: any) => part.type === "text" && part.text?.includes("<!-- IMAGE_GENERATION_END -->")
|
||
)
|
||
);
|
||
const assistantMessagesAfterUser = lastUserIndexForMarkers >= 0
|
||
? messages.slice(lastUserIndexForMarkers + 1).filter((msg) => msg.role === "assistant")
|
||
: messages.filter((msg) => msg.role === "assistant");
|
||
const hasImageContent = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some(
|
||
(part: any) =>
|
||
part.type === "text" &&
|
||
part.text &&
|
||
(part.text.includes("![thumb|") ||
|
||
part.text.includes("![thumb|||") ||
|
||
part.text.includes("![Generated Image"))
|
||
)
|
||
);
|
||
const hasAssistantTextReplyWithContent = assistantMessagesAfterUser.some((msg) =>
|
||
msg.parts?.some((part: any) => {
|
||
if (part.type !== "text" || !part.text) return false;
|
||
return stripInternalMarkers(part.text).length > 0;
|
||
})
|
||
);
|
||
const shouldWaitForAgentReplyBeforeImagePlaceholder =
|
||
(isAutoMode || showGemini || showVolcano || showAlibabaImage) &&
|
||
!hasAssistantTextReplyWithContent;
|
||
const isAutoImageFlow =
|
||
isAutoMode &&
|
||
(isGeneratingImageFlag || hasGeneratingMarker || hasGenerationEnd || hasImageContent);
|
||
|
||
if (showVideo) return <QianwenVideoLoading />;
|
||
if (shouldWaitForAgentReplyBeforeImagePlaceholder) {
|
||
return <ThinkingMessage />;
|
||
}
|
||
if (showGemini) return <GeminiImageLoading imageCount={imageCount} />;
|
||
if (showVolcano) return <VolcanoVisionLoading imageCount={imageCount} />;
|
||
if (showAlibabaImage) {
|
||
return (
|
||
<ImageGenerationLoading
|
||
imageCount={imageCount}
|
||
maxImages={6}
|
||
providerType={
|
||
currentModelProviderType === "ruxa-image"
|
||
? "ruxa-image"
|
||
: "alibaba-image"
|
||
}
|
||
modelName={currentModelDisplayName}
|
||
phase={imageGenerationPhase}
|
||
elapsedSeconds={imageGenerationElapsedSeconds}
|
||
/>
|
||
);
|
||
}
|
||
|
||
// 🆕 智能自动模式占位图
|
||
if (isAutoImageFlow) {
|
||
// 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图
|
||
// isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false
|
||
if (
|
||
autoImagePlaceholderReady &&
|
||
isGeneratingImageFlag &&
|
||
hasAssistantTextReplyWithContent &&
|
||
!hasGenerationEnd
|
||
) {
|
||
return (
|
||
<ImageGenerationLoading
|
||
imageCount={imageCount}
|
||
maxImages={6}
|
||
providerType="alibaba-image"
|
||
modelName="智能处理"
|
||
phase={imageGenerationPhase}
|
||
elapsedSeconds={imageGenerationElapsedSeconds}
|
||
/>
|
||
);
|
||
}
|
||
// 否则显示思考动画(在等待 AI 分析或延迟期间)
|
||
return <ThinkingMessage />;
|
||
}
|
||
|
||
// 其他情况显示通用思考动画
|
||
return <ThinkingMessage />;
|
||
})()}
|
||
|
||
<div
|
||
className="min-h-[24px] min-w-[24px] shrink-0"
|
||
ref={messagesEndRef}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
<button
|
||
aria-label="滚动至底部"
|
||
className={`-translate-x-1/2 absolute bottom-4 left-1/2 z-10 rounded-full border bg-background p-2 shadow-lg transition-all hover:bg-muted ${
|
||
isAtBottom
|
||
? "pointer-events-none scale-0 opacity-0"
|
||
: "pointer-events-auto scale-100 opacity-100"
|
||
}`}
|
||
onClick={() => scrollToBottom("smooth")}
|
||
type="button"
|
||
>
|
||
<ArrowDownIcon className="size-4" />
|
||
</button>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
export const Messages = memo(PureMessages, (prevProps: MessagesProps, nextProps: MessagesProps) => {
|
||
if (prevProps.isArtifactVisible && nextProps.isArtifactVisible) {
|
||
return true;
|
||
}
|
||
|
||
if (prevProps.status !== nextProps.status) {
|
||
return false;
|
||
}
|
||
if (prevProps.selectedModelId !== nextProps.selectedModelId) {
|
||
return false;
|
||
}
|
||
// 🆕 检查持久化生成状态变化
|
||
if (!equal(prevProps.pendingGeneration, nextProps.pendingGeneration)) {
|
||
return false;
|
||
}
|
||
if (prevProps.messages.length !== nextProps.messages.length) {
|
||
return false;
|
||
}
|
||
if (!equal(prevProps.messages, nextProps.messages)) {
|
||
return false;
|
||
}
|
||
if (!equal(prevProps.votes, nextProps.votes)) {
|
||
return false;
|
||
}
|
||
|
||
return false;
|
||
});
|