diff --git a/components/chat.tsx b/components/chat.tsx index 17750dd..ffe5445 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -37,6 +37,8 @@ import type { VisibilityType } from "@/components/visibility-selector"; import type { VideoGenerationParams } from "@/components/video-params-bar"; import type { ImageGenerationParams } from "@/components/image-params-bar"; +type ImageGenerationPhase = "idle" | "queued" | "processing" | "finalizing" | "failed"; + function normalizeImageParamsForProvider( params: ImageGenerationParams, providerType?: string @@ -71,6 +73,18 @@ function sortMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] { }); } +function hasImageMarkdown(parts?: Array) { + if (!parts || parts.length === 0) return false; + return parts.some( + (part) => + part?.type === "text" && + typeof part.text === "string" && + (part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image")) + ); +} + export function Chat({ id, initialMessages, @@ -106,6 +120,9 @@ export function Chat({ // 🆕 跟踪是否正在生成图片(用于占位图显示) const [isGeneratingImageFlag, setIsGeneratingImageFlag] = useState(false); + const [imageGenerationPhase, setImageGenerationPhase] = useState("idle"); + const [imageGenerationStartedAt, setImageGenerationStartedAt] = useState(null); + const imageGenerationPhaseRef = useRef("idle"); const imagePlaceholderTimerRef = useRef | null>(null); // 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突 @@ -135,6 +152,20 @@ export function Chat({ } }, [pendingGeneration, initialMessages, completeGeneration, router]); + const updateImageGenerationPhase = useCallback((nextPhase: ImageGenerationPhase) => { + imageGenerationPhaseRef.current = nextPhase; + setImageGenerationPhase(nextPhase); + + if (nextPhase === "idle" || nextPhase === "failed") { + setIsGeneratingImageFlag(false); + setImageGenerationStartedAt(null); + return; + } + + setIsGeneratingImageFlag(true); + setImageGenerationStartedAt((previous) => previous ?? Date.now()); + }, []); + // 清理占位图定时器 useEffect(() => { return () => { @@ -369,12 +400,12 @@ export function Chat({ }); if (hasImageGenEndMarker && isMarkerOnlyMessage) { - console.log("[Chat] 🖼️ Image generation ended, hide placeholder"); + console.log("[Chat] 🖼️ Image generation entered finalizing stage"); if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } - setIsGeneratingImageFlag(false); + updateImageGenerationPhase("finalizing"); return; // 不渲染标记消息 } @@ -384,7 +415,7 @@ export function Chat({ clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } - setIsGeneratingImageFlag(true); + updateImageGenerationPhase("processing"); return; // 不渲染标记消息 } @@ -405,7 +436,7 @@ export function Chat({ } imagePlaceholderTimerRef.current = setTimeout(() => { console.log("[Chat] 🖼️ Now showing placeholder after 3s delay"); - setIsGeneratingImageFlag(true); + updateImageGenerationPhase("processing"); imagePlaceholderTimerRef.current = null; }, 3000); } @@ -414,13 +445,7 @@ export function Chat({ const messageText = messageData.parts?.[0]?.text || ""; console.log("[Chat] 📝 Message text preview:", messageText.substring(0, 100)); - const hasImage = messageData.parts?.some( - (part: any) => part.type === "text" && part.text && ( - part.text.includes("![thumb|") || - part.text.includes("![thumb|||") || - part.text.includes("![Generated Image") - ) - ); + const hasImage = hasImageMarkdown(messageData.parts); console.log("[Chat] 🔍 hasImage check:", hasImage); if (hasImage) { @@ -429,7 +454,7 @@ export function Chat({ clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } - setIsGeneratingImageFlag(false); + updateImageGenerationPhase("idle"); } // 🆕 将消息添加到 appendedMessages 状态,而不是直接修改 useChat 的 messages @@ -452,6 +477,21 @@ export function Chat({ console.log("[Chat] Adding to appendedMessages:", messageData.id); return [...prev, newMessage]; }); + + const messageTextForErrorCheck = messageData.parts + ?.filter((part: any) => part.type === "text" && typeof part.text === "string") + .map((part: any) => part.text) + .join("\n") + .trim(); + const isLikelyGenerationError = + !hasImage && + !!messageTextForErrorCheck && + /失败|超时|重试|error|failed/i.test(messageTextForErrorCheck) && + imageGenerationPhaseRef.current !== "idle"; + if (isLikelyGenerationError) { + console.log("[Chat] ❌ Generation error detected, reset loading state"); + updateImageGenerationPhase("failed"); + } // 🆕 收到消息,清除持久化状态 completeGeneration(); @@ -469,39 +509,68 @@ export function Chat({ // 🆕 从数据库重新加载消息,确保 data-appendMessage 的消息正确显示 // 这是根治方案 B:后端已保存消息到数据库,前端从数据库重新加载 try { - // 延迟一小段时间确保数据库写入完成 - await new Promise(resolve => setTimeout(resolve, 500)); - - // 获取最新消息 - const response = await fetch(`/api/messages?chatId=${id}&_t=${Date.now()}`, { - cache: "no-store", - }); - if (response.ok) { - const data = await response.json(); - if (data.messages && Array.isArray(data.messages)) { - console.log("[Chat] Reloading messages from database:", data.messages.length); - // 合并消息,避免数据库短暂延迟导致实时 append 的图片消息被覆盖丢失 - setMessages((prev) => { - const mergedMap = new Map(); - prev.forEach((message) => mergedMap.set(message.id, message)); - (data.messages as ChatMessage[]).forEach((message) => mergedMap.set(message.id, message)); - return sortMessagesByCreatedAt(Array.from(mergedMap.values())); - }); + const mergeMessagesFromDatabase = (dbMessages: ChatMessage[]) => { + setMessages((prev) => { + const mergedMap = new Map(); + prev.forEach((message) => mergedMap.set(message.id, message)); + dbMessages.forEach((message) => mergedMap.set(message.id, message)); + return sortMessagesByCreatedAt(Array.from(mergedMap.values())); + }); - // 仅清理已经成功落库的 appended 消息,保留仍未在数据库中出现的实时消息 - const persistedIds = new Set( - (data.messages as ChatMessage[]).map((message) => message.id) - ); - setAppendedMessages((prev) => - prev.filter((message) => !persistedIds.has(message.id)) - ); + const persistedIds = new Set(dbMessages.map((message) => message.id)); + setAppendedMessages((prev) => prev.filter((message) => !persistedIds.has(message.id))); + }; + + const reloadMessagesOnce = async () => { + const response = await fetch(`/api/messages?chatId=${id}&_t=${Date.now()}`, { + cache: "no-store", + }); + if (!response.ok) return { hasImage: false }; + + const data = await response.json(); + if (!data.messages || !Array.isArray(data.messages)) { + return { hasImage: false }; } + + const dbMessages = data.messages as ChatMessage[]; + console.log("[Chat] Reloading messages from database:", dbMessages.length); + mergeMessagesFromDatabase(dbMessages); + + const hasPersistedImage = dbMessages.some((message) => hasImageMarkdown((message as any).parts)); + return { hasImage: hasPersistedImage }; + }; + + // 延迟一小段时间确保数据库写入完成 + await new Promise((resolve) => setTimeout(resolve, 500)); + + let { hasImage } = await reloadMessagesOnce(); + if (!hasImage && imageGenerationPhaseRef.current === "finalizing") { + for (let attempt = 1; attempt <= 6; attempt++) { + await new Promise((resolve) => setTimeout(resolve, 600)); + const result = await reloadMessagesOnce(); + hasImage = result.hasImage; + if (hasImage) break; + } + } + + if ( + hasImage || + (imageGenerationPhaseRef.current !== "idle" && + imageGenerationPhaseRef.current !== "failed") + ) { + updateImageGenerationPhase("idle"); } } catch (error) { console.error("[Chat] Failed to reload messages:", error); } }, onError: (error) => { + if (imagePlaceholderTimerRef.current) { + clearTimeout(imagePlaceholderTimerRef.current); + imagePlaceholderTimerRef.current = null; + } + updateImageGenerationPhase("failed"); + if (error instanceof ChatSDKError) { // Check if it's a credit card error if ( @@ -551,8 +620,12 @@ export function Chat({ console.log(`[Chat] 🚀 记录生成任务: modelType=${modelType}, modelId=${currentModelId}`); startGeneration(currentModelId, modelType); + + if (modelType === "gemini" || modelType === "volcano-vision" || modelType === "alibaba-image") { + updateImageGenerationPhase("queued"); + } } - }, [status, currentModelId, startGeneration]); + }, [status, currentModelId, startGeneration, updateImageGenerationPhase]); const searchParams = useSearchParams(); const query = searchParams.get("query"); @@ -1428,6 +1501,8 @@ export function Chat({ pendingGeneration={pendingGeneration} imageCount={imageParams?.imageCount || 1} isGeneratingImageFlag={isGeneratingImageFlag} + imageGenerationPhase={imageGenerationPhase} + imageGenerationStartedAt={imageGenerationStartedAt} />
diff --git a/components/image-generation-loading.tsx b/components/image-generation-loading.tsx index 22974fb..4c68896 100644 --- a/components/image-generation-loading.tsx +++ b/components/image-generation-loading.tsx @@ -85,6 +85,8 @@ interface ImageGenerationLoadingProps { colorTheme?: ColorTheme; // 颜色主题,默认 purple modelName?: string; // 模型名称,用于显示 providerType?: string; // 提供商类型,用于自动选择颜色 + phase?: "idle" | "queued" | "processing" | "finalizing" | "failed"; + elapsedSeconds?: number; } /** @@ -97,6 +99,8 @@ export const ImageGenerationLoading = memo(({ colorTheme, modelName, providerType, + phase = "processing", + elapsedSeconds = 0, }: ImageGenerationLoadingProps) => { // 根据 providerType 自动选择颜色主题 const getAutoColorTheme = (): ColorTheme => { @@ -124,6 +128,20 @@ export const ImageGenerationLoading = memo(({ const displayName = modelName || (providerType === "alibaba-image" ? "千问图片编辑" : providerType === "ruxa-image" ? "RUXA 图片编辑" : providerType === "volcano-vision" ? "即梦4.5" : "AI"); + + const phaseTitle = + phase === "queued" + ? "正在排队准备生成" + : phase === "finalizing" + ? "图片生成完成,正在加载结果" + : "正在生成图片"; + const phaseHint = + phase === "queued" + ? `${displayName} 正在排队,请稍候...` + : phase === "finalizing" + ? `${displayName} 正在同步结果到聊天记录...` + : `${displayName} 正在创作 ${count} 张图片,请稍候...`; + const elapsedLabel = elapsedSeconds > 0 ? `已等待 ${elapsedSeconds} 秒` : null; return (
- 正在生成 {count} 张图片 + {phaseTitle} {/* 提示文字 */} - - {displayName} 正在创作 {count} 张图片,请稍候... - +
+ {phaseHint} + {elapsedLabel ? {elapsedLabel} : null} +
diff --git a/components/messages.tsx b/components/messages.tsx index 94c39b4..d577ae8 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -51,6 +51,8 @@ type MessagesProps = { pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态 imageCount?: number; // 🆕 用户选择的图片数量 isGeneratingImageFlag?: boolean; // 🆕 正在生成图片的标志(来自 Chat 组件) + imageGenerationPhase?: "idle" | "queued" | "processing" | "finalizing" | "failed"; + imageGenerationStartedAt?: number | null; }; function PureMessages({ @@ -78,6 +80,8 @@ function PureMessages({ pendingGeneration, imageCount = 1, isGeneratingImageFlag = false, + imageGenerationPhase = "idle", + imageGenerationStartedAt = null, }: MessagesProps) { const { containerRef: messagesContainerRef, @@ -120,6 +124,9 @@ function PureMessages({ 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( @@ -135,6 +142,11 @@ function PureMessages({ const [showThinkingWithDelay, setShowThinkingWithDelay] = useState(false); const [autoImagePlaceholderReady, setAutoImagePlaceholderReady] = useState(false); const autoImagePlaceholderTimerRef = useRef | null>(null); + const [imageGenerationElapsedSeconds, setImageGenerationElapsedSeconds] = useState(0); + const isImageGenerationActive = + imageGenerationPhase === "queued" || + imageGenerationPhase === "processing" || + imageGenerationPhase === "finalizing"; useEffect(() => { return () => { @@ -144,6 +156,23 @@ function PureMessages({ } }; }, []); + + 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; @@ -523,8 +552,8 @@ function PureMessages({ isGeneratingImageFlag, }); - // 显示条件:正常等待动画 或 恢复的待处理状态 - const shouldShow = showThinkingWithDelay || hasRestoredPending; + // 显示条件:分阶段图片生成状态 / 正常等待动画 / 恢复的待处理状态 + const shouldShow = isImageGenerationActive || showThinkingWithDelay || hasRestoredPending; if (!shouldShow) { console.log("[Messages] 不显示占位图: shouldShow=false"); @@ -573,14 +602,43 @@ function PureMessages({ if (showVideo) return ; if (showGemini) return ; if (showVolcano) return ; - if (showAlibabaImage) return ; + if (showAlibabaImage) { + return ( + + ); + } // 🆕 智能自动模式占位图 if (isAutoMode && userMessageHasImage) { // 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图 // isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false - if (autoImagePlaceholderReady && isGeneratingImageFlag && hasAssistantTextReplyWithContent && !hasGenerationEnd) { - return ; + if ( + autoImagePlaceholderReady && + isGeneratingImageFlag && + hasAssistantTextReplyWithContent && + !hasGenerationEnd + ) { + return ( + + ); } // 否则显示思考动画(在等待 AI 分析或延迟期间) return ;