lovart3/components/messages.tsx
Your Name 2a14ae8445 fix: 过滤清理后为空的消息,避免显示空对话框
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-01 16:42:23 +08:00

525 lines
21 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.

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;
isProcessingEnhance?: boolean;
isProcessingRmbg?: boolean;
isProcessingExtend?: boolean;
isProcessingAngleEdit?: boolean;
pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态
imageCount?: number; // 🆕 用户选择的图片数量
isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件)
};
function PureMessages({
chatId,
status,
votes,
messages,
setMessages,
regenerate,
sendMessage,
isReadonly,
selectedModelId,
onImageEdit,
onDrawingComplete,
onImageEnhance,
onImageRmbg,
onImageExtend,
onImageAngleEdit,
isProcessingEnhance = false,
isProcessingRmbg = false,
isProcessingExtend = false,
isProcessingAngleEdit = false,
pendingGeneration,
imageCount = 1,
isGeneratingImageFlag = false,
}: MessagesProps) {
const {
containerRef: messagesContainerRef,
endRef: messagesEndRef,
isAtBottom,
scrollToBottom,
hasSentMessage,
} = useMessages({
status,
});
useDataStream();
// 获取模型列表以判断当前模型类型
const { data: modelsData } = useSWR<{ models: DynamicModel[] }>(
"/api/models",
fetcher
);
// 判断当前选中的模型类型
const isGeminiModel = modelsData?.models?.some(
(m: DynamicModel) => m.id === selectedModelId && m.providerType === "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"
) ?? false;
// 🆕 获取当前模型的显示名称
const currentModelDisplayName = modelsData?.models?.find(
(m: DynamicModel) => m.id === selectedModelId
)?.displayName;
// 判断是否是视频生成模型(通过 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);
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 isGeneratingImage = messages.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
)
);
// 对于图片模型,检查是否有图片内容(包含 ![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 的文字回复(包含 GENERATING_IMAGE 标记)
const hasAssistantTextReply = messages.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
)
);
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 {
// 非智能自动模式的原有逻辑
if (status === "submitted") {
setShowThinkingWithDelay(true);
} else if (status === "streaming" && lastMessage?.role === "assistant" && !hasContent) {
setShowThinkingWithDelay(true);
} else if (status === "streaming" && lastMessage?.role === "assistant" && hasContent) {
const timer = setTimeout(() => {
setShowThinkingWithDelay(false);
}, delayTime);
return () => clearTimeout(timer);
} else {
setShowThinkingWithDelay(false);
}
}
}, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel, isAlibabaImageModel, selectedModelId, isGeneratingImageFlag]);
// 🆕 监听新消息添加,当用户发送带图片的消息或添加加载消息时,强制滚动到底部
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, "").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}
isProcessingEnhance={isProcessingEnhance}
isProcessingRmbg={isProcessingRmbg}
isProcessingExtend={isProcessingExtend}
isProcessingAngleEdit={isProcessingAngleEdit}
/>
);
})}
{/* 等待动画:根据模型类型显示不同的加载效果 */}
{/* 🆕 支持恢复的生成状态(用户离开再返回) */}
{(() => {
// 检查最后一条消息是否是用户消息(表示还在等待 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) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
)
);
const hasRestoredPending = pendingGeneration && isWaitingForResponse && !hasAssistantReply;
// 🔍 调试日志
console.log("[Messages] 占位图状态检查:", {
pendingGeneration,
lastMessageRole: lastMessage?.role,
isWaitingForResponse,
hasRestoredPending,
hasAssistantReply,
showThinkingWithDelay,
messagesCount: messages.length,
isGeneratingImageFlag,
});
// 显示条件:正常等待动画 或 恢复的待处理状态
const shouldShow = 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";
const lastUserMsg = [...messages].reverse().find(m => m.role === "user");
const userMessageHasImage = lastUserMsg?.parts?.some((p: any) => p.type === "file" || p.type === "image");
// 🆕 检查是否收到了 GENERATING_IMAGE 标记(表示对话已发送,开始生图)
const hasGeneratingMarker = messages.some(msg =>
msg.role === "assistant" && msg.parts?.some(
(part: any) => part.type === "text" && part.text?.includes("<!-- GENERATING_IMAGE -->")
)
);
if (showVideo) return <QianwenVideoLoading />;
if (showGemini) return <GeminiImageLoading imageCount={imageCount} />;
if (showVolcano) return <VolcanoVisionLoading imageCount={imageCount} />;
if (showAlibabaImage) return <ImageGenerationLoading imageCount={imageCount} maxImages={6} providerType="alibaba-image" modelName={currentModelDisplayName} />;
// 🆕 智能自动模式占位图
if (isAutoMode && userMessageHasImage) {
// 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图
// isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false
if (isGeneratingImageFlag) {
return <ImageGenerationLoading imageCount={imageCount} maxImages={6} providerType="alibaba-image" modelName="智能处理" />;
}
// 否则显示思考动画(在等待 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;
});