"use client"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { useRouter, useSearchParams } from "next/navigation"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; import useSWR, { useSWRConfig } from "swr"; import { unstable_serialize } from "swr/infinite"; import { ChatHeader } from "@/components/chat-header"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { useArtifactSelector } from "@/hooks/use-artifact"; import { useAutoResume } from "@/hooks/use-auto-resume"; import { useChatVisibility } from "@/hooks/use-chat-visibility"; import { usePendingGeneration, type PendingGeneration } from "@/hooks/use-pending-generation"; import type { Vote } from "@/lib/db/schema"; import type { UserType } from "@/app/(auth)/auth"; import { ChatSDKError } from "@/lib/errors"; import type { Attachment, ChatMessage } from "@/lib/types"; import type { AppUsage } from "@/lib/usage"; import { fetcher, fetchWithErrorHandlers, generateUUID } from "@/lib/utils"; import { Artifact } from "@/components/artifact"; 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 "sonner"; import type { VisibilityType } from "@/components/visibility-selector"; import type { VideoGenerationParams } from "@/components/video-params-bar"; import type { ImageGenerationParams } from "@/components/image-params-bar"; export function Chat({ id, initialMessages, initialChatModel, initialVisibilityType, isReadonly, autoResume, initialLastContext, userType, }: { id: string; initialMessages: ChatMessage[]; initialChatModel: string; initialVisibilityType: VisibilityType; isReadonly: boolean; autoResume: boolean; initialLastContext?: AppUsage; userType?: UserType; }) { const router = useRouter(); const { visibilityType } = useChatVisibility({ chatId: id, initialVisibilityType, }); // 🆕 持久化生成任务状态(解决用户离开再返回时加载状态丢失) const { pendingGeneration, startGeneration, completeGeneration } = usePendingGeneration(id); // 🆕 标记是否已经尝试刷新过(避免无限刷新) const hasTriedRefreshRef = useRef(false); // 🆕 跟踪是否正在生成图片(用于占位图显示) const [isGeneratingImageFlag, setIsGeneratingImageFlag] = useState(false); // 🆕 存储通过 data-appendMessage 添加的消息,避免与 useChat 内部状态冲突 const [appendedMessages, setAppendedMessages] = useState([]); // 🆕 当检测到恢复的 pendingGeneration 时,刷新页面获取最新消息 useEffect(() => { if (pendingGeneration && !hasTriedRefreshRef.current) { // 检查最后一条消息是否已经是 AI 回复 const lastMessage = initialMessages.length > 0 ? initialMessages[initialMessages.length - 1] : null; if (lastMessage?.role === "assistant") { // 最后一条已经是 AI 回复,说明生成已完成,清除 pending 状态 console.log("[Chat] 🎉 检测到生成已完成(最后消息是 AI 回复),清除 pending 状态"); completeGeneration(); } else if (lastMessage?.role === "user") { // 最后一条是用户消息,可能还在生成中,或者已经完成但页面数据过期 // 标记已尝试刷新,然后刷新页面获取最新数据 hasTriedRefreshRef.current = true; console.log("[Chat] 🔄 检测到恢复的 pending 状态,刷新页面获取最新消息..."); // 延迟一小段时间再刷新,避免立即刷新 setTimeout(() => { router.refresh(); }, 500); } } }, [pendingGeneration, initialMessages, completeGeneration, router]); const { mutate } = useSWRConfig(); // Handle browser back/forward navigation useEffect(() => { const handlePopState = () => { // When user navigates back/forward, refresh to sync with URL router.refresh(); }; window.addEventListener("popstate", handlePopState); return () => window.removeEventListener("popstate", handlePopState); }, [router]); const { setDataStream } = useDataStream(); const [input, setInput] = useState(""); const [usage, setUsage] = useState(initialLastContext); const [showCreditCardAlert, setShowCreditCardAlert] = useState(false); const [currentModelId, setCurrentModelId] = useState(initialChatModel); // 图片处理状态,防止重复点击 const [isProcessingEnhance, setIsProcessingEnhance] = useState(false); const [isProcessingRmbg, setIsProcessingRmbg] = useState(false); const [isProcessingExtend, setIsProcessingExtend] = useState(false); const [isProcessingAngleEdit, setIsProcessingAngleEdit] = useState(false); const currentModelIdRef = useRef(currentModelId); // 🆕 视频参数状态 const [videoParams, setVideoParams] = useState(null); // 🆕 图片参数状态(默认值:auto比例,2K分辨率) const [imageParams, setImageParams] = useState({ aspectRatio: "auto", imageSize: "2K" }); // 🆕 从 localStorage 读取保存的图片参数 useEffect(() => { try { const saved = localStorage.getItem("gemini-image-params"); if (saved) { const parsed = JSON.parse(saved); // 确保所有字段都有值,避免传递 undefined setImageParams({ aspectRatio: "auto", imageSize: "2K", imageCount: 1, ...parsed, }); } } catch (e) { console.error("Failed to load image params:", e); } }, []); // 🆕 保存图片参数到 localStorage const handleImageParamsChange = (params: ImageGenerationParams) => { setImageParams(params); try { localStorage.setItem("gemini-image-params", JSON.stringify(params)); } catch (e) { console.error("Failed to save image params:", e); } console.log("[Chat] Image params updated:", params); }; // 🆕 获取模型列表以检查是否是视频模型/图片模型 const { data: modelsData } = useSWR<{ models: Array<{ id: string; modelId: string; displayName: string; providerType: string; supportsVision?: boolean; supportsImageGen?: boolean; }>; }>("/api/models", fetcher); const currentModel = modelsData?.models?.find(m => m.id === currentModelId); const isVideoModel = currentModel?.providerType === "volcano-video"; const isImageModel = currentModel?.supportsImageGen === true; // 🔍 调试日志 console.log('[Chat] Render - currentModelId:', currentModelId); console.log('[Chat] Render - modelsData loaded:', !!modelsData); console.log('[Chat] Render - currentModel:', currentModel?.displayName, { id: currentModel?.id, supportsImageGen: currentModel?.supportsImageGen, providerType: currentModel?.providerType, isImageModel, imageParams }); // 使用 ref 保存视频/图片相关状态,以便在 prepareSendMessagesRequest 中访问最新值 const isVideoModelRef = useRef(isVideoModel); const videoParamsRef = useRef(videoParams); const isImageModelRef = useRef(isImageModel); const imageParamsRef = useRef(imageParams); useEffect(() => { isVideoModelRef.current = isVideoModel; videoParamsRef.current = videoParams; isImageModelRef.current = isImageModel; imageParamsRef.current = imageParams; console.log("[Chat] Refs updated - imageParams:", imageParams); }, [isVideoModel, videoParams, isImageModel, imageParams]); // 调试日志 console.log("[Chat Debug] currentModelId:", currentModelId); console.log("[Chat Debug] currentModel:", currentModel); console.log("[Chat Debug] isVideoModel:", isVideoModel); // 🆕 当切换到视频模型时,自动初始化 videoParams useEffect(() => { if (isVideoModel && !videoParams) { const defaultParams: VideoGenerationParams = { mode: "text-to-video", resolution: "720p", ratio: "16:9", duration: 5, watermark: false, cameraFixed: false, cameraMove: "none", fps: 24, }; setVideoParams(defaultParams); console.log("[Chat] 视频模型已选择,初始化默认参数:", defaultParams); } else if (!isVideoModel && videoParams) { // 切换到非视频模型时清空 videoParams setVideoParams(null); console.log("[Chat] 已切换到非视频模型,清空视频参数"); } }, [isVideoModel, videoParams]); // 🆕 当切换到图片模型时,强制更新 imageParams(确保组件重新渲染) useEffect(() => { if (isImageModel) { // 强制更新一次 imageParams 以触发重新渲染 setImageParams(prev => ({...prev})); console.log("[Chat] 图片模型已激活,触发参数更新"); } }, [isImageModel]); useEffect(() => { currentModelIdRef.current = currentModelId; // 调试:在控制台显示当前模型切换 console.log(`[Chat] Model changed to: ${currentModelId}`); }, [currentModelId]); const { messages, setMessages, sendMessage, status, stop, regenerate, resumeStream, } = useChat({ id, messages: initialMessages, experimental_throttle: 100, generateId: generateUUID, transport: new DefaultChatTransport({ api: "/api/chat", fetch: fetchWithErrorHandlers, prepareSendMessagesRequest(request) { console.log("[Chat] Preparing request, videoParams:", videoParamsRef.current); console.log("[Chat] Preparing request, imageParams:", imageParamsRef.current); console.log("[Chat] Is video model:", isVideoModelRef.current); console.log("[Chat] Is image model:", isImageModelRef.current); return { body: { id: request.id, message: request.messages.at(-1), selectedChatModel: currentModelIdRef.current, selectedVisibilityType: visibilityType, videoParams: videoParamsRef.current || undefined, // 🆕 传递视频参数(使用ref获取最新值) imageParams: imageParamsRef.current || undefined, // 🆕 传递图片参数(使用ref获取最新值) ...request.body, }, }; }, }), onData: (dataPart: any) => { setDataStream((ds) => (ds ? [...ds, dataPart] : [])); if (dataPart.type === "data-usage") { setUsage(dataPart.data); } // 🆕 处理后端返回的自定义消息(用于图片/视频生成的非流式响应) if (dataPart.type === "data-appendMessage") { try { const messageData = typeof dataPart.data === 'string' ? JSON.parse(dataPart.data) : dataPart.data; console.log("[Chat] Received appendMessage:", messageData); // 🆕 检测是否包含 GENERATING_IMAGE 标记 const hasGeneratingMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); if (hasGeneratingMarker) { console.log("[Chat] 🖼️ Detected GENERATING_IMAGE marker, will show placeholder after delay"); // 🆕 延迟显示占位图,让用户先看到 AI 的文字回复 // 消息会立即添加到 appendedMessages 并显示,3 秒后再显示动态占位图 setTimeout(() => { console.log("[Chat] 🖼️ Now showing placeholder after 3s delay"); setIsGeneratingImageFlag(true); }, 3000); // 延迟 3 秒,让用户有时间阅读文字 } // 🆕 检测是否包含图片(生成完成) 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") ) ); console.log("[Chat] 🔍 hasImage check:", hasImage); if (hasImage) { console.log("[Chat] 🎉 Image generated, hiding placeholder"); setIsGeneratingImageFlag(false); } // 🆕 将消息添加到 appendedMessages 状态,而不是直接修改 useChat 的 messages // 这样可以避免与 useChat 内部状态冲突导致消息丢失 const newMessage: ChatMessage = { id: messageData.id, role: messageData.role, content: messageData.parts?.[0]?.text || "", createdAt: new Date(), parts: messageData.parts || [], attachments: [], } as ChatMessage; setAppendedMessages((prev) => { // 检查消息是否已存在 if (prev.some(m => m.id === messageData.id)) { console.log("[Chat] Appended message already exists, skipping:", messageData.id); return prev; } console.log("[Chat] Adding to appendedMessages:", messageData.id); return [...prev, newMessage]; }); // 🆕 收到消息,清除持久化状态 completeGeneration(); } catch (e) { console.error("[Chat] Failed to parse data-appendMessage:", e); } } }, onFinish: async () => { mutate(unstable_serialize(getChatHistoryPaginationKey)); // 🆕 生成完成,清除持久化状态 completeGeneration(); console.log("[Chat] onFinish called"); // 🆕 从数据库重新加载消息,确保 data-appendMessage 的消息正确显示 // 这是根治方案 B:后端已保存消息到数据库,前端从数据库重新加载 try { // 延迟一小段时间确保数据库写入完成 await new Promise(resolve => setTimeout(resolve, 500)); // 获取最新消息 const response = await fetch(`/api/messages?chatId=${id}`); 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); // 更新消息状态 setMessages(data.messages); // 清空 appendedMessages,因为已经从数据库加载了完整消息 setAppendedMessages([]); } } } catch (error) { console.error("[Chat] Failed to reload messages:", error); } }, onError: (error) => { if (error instanceof ChatSDKError) { // Check if it's a credit card error if ( error.message?.includes("AI Gateway requires a valid credit card") ) { setShowCreditCardAlert(true); } else { toast.error(error.message); } } }, }); // 🆕 保存 initialMessages 的引用,防止历史消息丢失 // 当 useChat 内部状态重置时,我们仍然有初始消息 const initialMessagesRef = useRef(initialMessages); useEffect(() => { // 只在初始化时设置,不在后续更新 if (initialMessagesRef.current.length === 0 && initialMessages.length > 0) { initialMessagesRef.current = initialMessages; } }, [initialMessages]); // 调试:监控聊天状态变化 & 持久化生成状态 // 使用 ref 来访问最新的 modelsData,避免依赖数组问题 const modelsDataRef = useRef(modelsData); useEffect(() => { modelsDataRef.current = modelsData; }, [modelsData]); useEffect(() => { console.log(`[Chat] Status changed: ${status}`); // 🆕 当状态变为 submitted 时,记录生成任务 if (status === "submitted") { // 判断模型类型 - 使用 ref 获取最新的 modelsData const models = modelsDataRef.current?.models; const model = models?.find((m: any) => m.id === currentModelId); let modelType: PendingGeneration["modelType"] = "other"; // 🆕 gemini-image 和 huanke-gemini 都视为 gemini 类型 if (model?.providerType === "gemini" || model?.providerType === "gemini-image" || model?.providerType === "huanke-gemini") modelType = "gemini"; else if (model?.providerType === "volcano-vision") modelType = "volcano-vision"; else if (model?.providerType === "volcano-video") modelType = "volcano-video"; else if (model?.providerType === "qianwen-video") modelType = "qianwen-video"; else if (model?.providerType === "alibaba-image") modelType = "alibaba-image"; console.log(`[Chat] 🚀 记录生成任务: modelType=${modelType}, modelId=${currentModelId}`); startGeneration(currentModelId, modelType); } }, [status, currentModelId, startGeneration]); const searchParams = useSearchParams(); const query = searchParams.get("query"); const editImageUrl = searchParams.get("editImage"); const [hasAppendedQuery, setHasAppendedQuery] = useState(false); const [hasLoadedEditImage, setHasLoadedEditImage] = useState(false); useEffect(() => { if (query && !hasAppendedQuery) { sendMessage({ role: "user" as const, parts: [{ type: "text", text: query }], }); setHasAppendedQuery(true); window.history.replaceState({}, "", `/chat/${id}`); } }, [query, sendMessage, hasAppendedQuery, id]); // 🆕 处理从Gallery跳转过来的图片编辑请求 useEffect(() => { if (editImageUrl && !hasLoadedEditImage) { const fileName = editImageUrl.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`; // 推断图片类型 let contentType = "image/jpeg"; if (fileName.toLowerCase().endsWith(".png")) contentType = "image/png"; if (fileName.toLowerCase().endsWith(".webp")) contentType = "image/webp"; if (fileName.toLowerCase().endsWith(".gif")) contentType = "image/gif"; setAttachments([{ url: decodeURIComponent(editImageUrl), name: fileName, contentType: contentType, }]); // 自动填充提示词 setInput("请帮我编辑这张图片:"); setHasLoadedEditImage(true); window.history.replaceState({}, "", `/chat/${id}`); } }, [editImageUrl, hasLoadedEditImage, id, setInput]); // 🆕 处理从导航栏图片编辑器跳转过来的图片编辑请求 const [hasLoadedPendingImage, setHasLoadedPendingImage] = useState(false); useEffect(() => { if (!hasLoadedPendingImage) { try { // 安全地访问sessionStorage(某些浏览器设置可能禁止访问) let pendingImage: string | null = null; try { pendingImage = sessionStorage.getItem("pending-edited-image"); } catch (storageError) { // sessionStorage不可用(如隐私模式),忽略错误继续执行 console.warn("sessionStorage不可用,跳过待处理图片加载:", storageError); } if (pendingImage) { // 清除 sessionStorage 中的数据,避免重复添加 try { sessionStorage.removeItem("pending-edited-image"); } catch (removeError) { // 忽略清除错误 } // 判断是 base64 data URL 还是普通 URL const fileName = pendingImage.startsWith("data:image/") ? `edited-image-${Date.now()}.png` : pendingImage.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`; // 推断图片类型 let contentType = "image/png"; if (pendingImage.startsWith("data:image/jpeg") || pendingImage.startsWith("data:image/jpg")) { contentType = "image/jpeg"; } else if (pendingImage.startsWith("data:image/webp")) { contentType = "image/webp"; } else if (pendingImage.startsWith("data:image/gif")) { contentType = "image/gif"; } else if (!pendingImage.startsWith("data:")) { // 普通 URL,根据文件名推断 if (fileName.toLowerCase().endsWith(".jpg") || fileName.toLowerCase().endsWith(".jpeg")) { contentType = "image/jpeg"; } else if (fileName.toLowerCase().endsWith(".webp")) { contentType = "image/webp"; } else if (fileName.toLowerCase().endsWith(".gif")) { contentType = "image/gif"; } } setAttachments((prev) => [ ...prev, { url: pendingImage, name: fileName, contentType: contentType, }, ]); // 自动填充提示词(如果输入框为空) setInput((prev) => prev || "请帮我处理这张图片:"); setHasLoadedPendingImage(true); } } catch (error) { console.error("读取待处理图片失败:", error); setHasLoadedPendingImage(true); } } }, [hasLoadedPendingImage, setInput]); const { data: votes } = useSWR( messages.length >= 2 ? `/api/vote?chatId=${id}` : null, fetcher ); const [attachments, setAttachments] = useState([]); const isArtifactVisible = useArtifactSelector((state) => state.isVisible); // 处理图片编辑请求 const handleImageEdit = (imageUrl: string) => { // 优化:直接使用 URL 构造附件对象,无需重新下载上传 // 后端 API 会在发送时处理 URL(如果需要 Base64 转换) const fileName = imageUrl.split('/').pop()?.split('?')[0] || `image-${Date.now()}.jpg`; // 简单的类型推断 let contentType = "image/jpeg"; if (fileName.toLowerCase().endsWith(".png")) contentType = "image/png"; if (fileName.toLowerCase().endsWith(".webp")) contentType = "image/webp"; if (fileName.toLowerCase().endsWith(".gif")) contentType = "image/gif"; setAttachments((prev) => [ ...prev, { url: imageUrl, name: fileName, contentType: contentType, }, ]); // 如果输入框为空,自动填入提示词 setInput((prev) => prev ? prev : "请帮我编辑这张图片:"); }; // 处理画图标注完成 const handleDrawingComplete = (annotatedImageUrl: string) => { // annotatedImageUrl 是 base64 data URL 格式 const fileName = `annotated-${Date.now()}.png`; setAttachments((prev) => [ ...prev, { url: annotatedImageUrl, name: fileName, contentType: "image/png", }, ]); // 如果输入框为空,自动填入提示词 setInput((prev) => prev ? prev : "请根据我的标注修改这张图片:"); }; // 处理图片变清晰 const handleImageEnhance = async (imageUrl: string) => { // 防止重复点击 if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { toast.info("正在处理中,请稍候..."); return; } setIsProcessingEnhance(true); const messageId = generateUUID(); // 创建临时加载消息 const loadingMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: "正在变清晰..." }], createdAt: new Date().toISOString(), } as ChatMessage; setMessages((prev) => [...prev, loadingMessage]); // 延迟滚动到底部,确保DOM已更新 setTimeout(() => { const messagesContainer = document.querySelector('[role="log"]'); if (messagesContainer) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth", }); } }, 100); 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 = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; // 创建最终消息(前端显示用) const finalMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: imageMarkdown }], metadata: { createdAt: new Date().toISOString(), }, } as ChatMessage; // 更新消息 setMessages((prev) => 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 { 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)); } finally { console.log("[变清晰] finally 块执行,重置 isProcessingEnhance"); setIsProcessingEnhance(false); } }; reader.onerror = () => { console.log("[变清晰] reader.onerror 执行,重置 isProcessingEnhance"); toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingEnhance(false); }; reader.readAsDataURL(blob); } catch (error) { console.error("[变清晰] 错误:", error); console.log("[变清晰] 外层 catch 执行,重置 isProcessingEnhance"); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingEnhance(false); } }; // 处理图片抠图 const handleImageRmbg = async (imageUrl: string) => { // 防止重复点击 if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { toast.info("正在处理中,请稍候..."); return; } setIsProcessingRmbg(true); const messageId = generateUUID(); // 创建临时加载消息 const loadingMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: "正在抠图..." }], createdAt: new Date().toISOString(), } as ChatMessage; setMessages((prev) => [...prev, loadingMessage]); // 延迟滚动到底部,确保DOM已更新 setTimeout(() => { const messagesContainer = document.querySelector('[role="log"]'); if (messagesContainer) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth", }); } }, 100); 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 = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; // 创建最终消息(前端显示用) const finalMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: imageMarkdown }], metadata: { createdAt: new Date().toISOString(), }, } as ChatMessage; // 更新消息 setMessages((prev) => 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 { 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)); } finally { console.log("[抠图] finally 块执行,重置 isProcessingRmbg"); setIsProcessingRmbg(false); } }; reader.onerror = () => { console.log("[抠图] reader.onerror 执行,重置 isProcessingRmbg"); toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingRmbg(false); }; reader.readAsDataURL(blob); } catch (error) { console.error("[抠图] 错误:", error); console.log("[抠图] 外层 catch 执行,重置 isProcessingRmbg"); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingRmbg(false); } }; // 处理图像拓展 const handleImageExtend = async (imageUrl: string) => { // 防止重复点击 if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend) { toast.info("正在处理中,请稍候..."); return; } setIsProcessingExtend(true); const messageId = generateUUID(); // 创建临时加载消息 const loadingMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: "正在图像拓展..." }], metadata: { createdAt: new Date().toISOString(), }, } as ChatMessage; setMessages((prev) => [...prev, loadingMessage]); // 延迟滚动到底部,确保DOM已更新 setTimeout(() => { const messagesContainer = document.querySelector('[role="log"]'); if (messagesContainer) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth", }); } }, 100); 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 extendResponse = await fetch("/api/extend", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_base64: base64Data, prompt: "图片图像拓展处理", }), }); if (!extendResponse.ok) { const error = await extendResponse.json(); throw new Error(error.error || "图像拓展处理失败"); } const result = await extendResponse.json(); if (result.success && result.original_url) { // 构造图片Markdown(使用与Gemini生成相同的格式) const imageMarkdown = `![thumb|||${result.original_url}|||2K](${result.thumbnail_url || result.original_url})`; // 创建最终消息(前端显示用) const finalMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: imageMarkdown }], metadata: { createdAt: new Date().toISOString(), }, } as ChatMessage; // 更新消息 setMessages((prev) => 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 { 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)); } finally { console.log("[图像拓展] finally 块执行,重置 isProcessingExtend"); setIsProcessingExtend(false); } }; reader.onerror = () => { console.log("[图像拓展] reader.onerror 执行,重置 isProcessingExtend"); toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingExtend(false); }; reader.readAsDataURL(blob); } catch (error) { console.error("[图像拓展] 错误:", error); console.log("[图像拓展] 外层 catch 执行,重置 isProcessingExtend"); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); setIsProcessingExtend(false); } }; // 处理角度编辑完成 const handleImageAngleEdit = async (resultUrl: string) => { const messageId = generateUUID(); // 构造图片Markdown(使用与其他处理功能相同的格式) const imageMarkdown = `![thumb|||${resultUrl}|||2K](${resultUrl})`; // 创建最终消息 const finalMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: imageMarkdown }], metadata: { createdAt: new Date().toISOString(), }, } as ChatMessage; // 添加消息 setMessages((prev) => [...prev, finalMessage]); // 滚动到底部 setTimeout(() => { const messagesContainer = document.querySelector('[role="log"]'); if (messagesContainer) { messagesContainer.scrollTo({ top: messagesContainer.scrollHeight, behavior: "smooth", }); } }, 100); // 保存消息到数据库 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 angle edit message:", saveError); } }; useAutoResume({ autoResume, initialMessages, resumeStream, setMessages, }); // 🆕 合并消息:初始消息 + useChat 消息 + appendedMessages // 确保历史消息不会因为 useChat 内部状态重置而丢失 const mergedMessages = useMemo(() => { // 使用 Map 进行去重,以 ID 为键 const messageMap = new Map(); // 1. 首先添加初始消息(历史记录) initialMessagesRef.current.forEach(m => { messageMap.set(m.id, m); }); // 2. 然后添加 useChat 的当前消息(会覆盖相同 ID 的旧消息) messages.forEach(m => { messageMap.set(m.id, m); }); // 3. 最后添加 appendedMessages(data-appendMessage 的消息) appendedMessages.forEach(m => { if (!messageMap.has(m.id)) { messageMap.set(m.id, m); } }); // 按创建时间排序 const result = Array.from(messageMap.values()).sort((a, b) => { const timeA = (a as any).createdAt ? new Date((a as any).createdAt).getTime() : 0; const timeB = (b as any).createdAt ? new Date((b as any).createdAt).getTime() : 0; return timeA - timeB; }); console.log("[Chat] Merged messages:", result.length, "(initial:", initialMessagesRef.current.length, ", current:", messages.length, ", appended:", appendedMessages.length, ")"); return result; }, [messages, appendedMessages]); return ( <>
{!isReadonly && ( { setVideoParams(params); console.log("[Chat] Video params updated:", params); }} isImageModel={isImageModel} imageModelName={currentModel?.displayName} imageParams={imageParams} onImageParamsChange={handleImageParamsChange} imageProviderType={currentModel?.providerType} /> )}
启用 AI Gateway 此应用需要 {process.env.NODE_ENV === "production" ? "项目拥有者" : "你"} 激活 Vercel AI Gateway。 取消 { window.open( "https://vercel.com/d?to=%2F%5Bteam%5D%2F%7E%2Fai%3Fmodal%3Dadd-credit-card", "_blank" ); window.location.href = "/"; }} > 立即启用 ); }