"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"; type ImageGenerationPhase = "idle" | "queued" | "processing" | "finalizing" | "failed"; function normalizeImageParamsForProvider( params: ImageGenerationParams, providerType?: string ): ImageGenerationParams { const normalized = { ...params }; const currentCount = normalized.imageCount || 1; if (providerType === "ruxa-image") { normalized.imageCount = 1; return normalized; } if (providerType === "alibaba-image") { normalized.imageCount = Math.min(Math.max(currentCount, 1), 6); return normalized; } if (providerType === "volcano-vision") { normalized.imageCount = Math.min(Math.max(currentCount, 1), 3); return normalized; } normalized.imageCount = Math.min(Math.max(currentCount, 1), 2); return normalized; } function sortMessagesByCreatedAt(messages: ChatMessage[]): ChatMessage[] { return [...messages].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; }); } 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, 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); const [imageGenerationPhase, setImageGenerationPhase] = useState("idle"); const [imageGenerationStartedAt, setImageGenerationStartedAt] = useState(null); const imageGenerationPhaseRef = useRef("idle"); const imagePlaceholderTimerRef = useRef | null>(null); // 🆕 存储通过 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 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 () => { if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } }; }, []); 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 [isProcessingWatermarkRemove, setIsProcessingWatermarkRemove] = 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) => { const normalized = normalizeImageParamsForProvider(params, currentModel?.providerType); setImageParams(normalized); try { localStorage.setItem("gemini-image-params", JSON.stringify(normalized)); } catch (e) { console.error("Failed to save image params:", e); } console.log("[Chat] Image params updated:", normalized); }; // 🆕 获取模型列表以检查是否是视频模型/图片模型 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) { setImageParams((prev) => { const next = normalizeImageParamsForProvider(prev, currentModel?.providerType); if (JSON.stringify(prev) === JSON.stringify(next)) return prev; try { localStorage.setItem("gemini-image-params", JSON.stringify(next)); } catch {} return next; }); console.log("[Chat] 图片模型已激活,参数已按当前模型规范化"); } }, [isImageModel, currentModel?.providerType]); 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); const hasImageGenStartMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); const hasImageGenEndMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); const hasLegacyGeneratingMarker = messageData.parts?.some( (part: any) => part.type === "text" && part.text?.includes("") ); const isMarkerOnlyMessage = messageData.parts?.every((part: any) => { if (part.type !== "text" || !part.text) return false; const cleanedText = part.text .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") .replace(/\n*\n*/g, "") .trim(); return cleanedText.length === 0; }); if (hasImageGenEndMarker && isMarkerOnlyMessage) { console.log("[Chat] 🖼️ Image generation entered finalizing stage"); if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } updateImageGenerationPhase("finalizing"); return; // 不渲染标记消息 } if (hasImageGenStartMarker && isMarkerOnlyMessage) { console.log("[Chat] 🖼️ Image generation started, show placeholder"); if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } updateImageGenerationPhase("processing"); return; // 不渲染标记消息 } if (hasLegacyGeneratingMarker) { console.log("[Chat] 🖼️ Detected legacy marker, will show placeholder after delay"); const hasDisplayText = messageData.parts?.some((part: any) => { if (part.type !== "text" || !part.text) return false; const cleanedText = part.text.replace(/\n*\n*/g, "").trim(); return cleanedText.length > 0; }); if (!hasDisplayText) { console.log("[Chat] ⚠️ Marker message has no display text, skip image placeholder timer"); return; } if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } imagePlaceholderTimerRef.current = setTimeout(() => { console.log("[Chat] 🖼️ Now showing placeholder after 3s delay"); updateImageGenerationPhase("processing"); imagePlaceholderTimerRef.current = null; }, 3000); } // 🆕 检测是否包含图片(生成完成) const messageText = messageData.parts?.[0]?.text || ""; console.log("[Chat] 📝 Message text preview:", messageText.substring(0, 100)); const hasImage = hasImageMarkdown(messageData.parts); console.log("[Chat] 🔍 hasImage check:", hasImage); if (hasImage) { console.log("[Chat] 🎉 Image generated, hiding placeholder"); if (imagePlaceholderTimerRef.current) { clearTimeout(imagePlaceholderTimerRef.current); imagePlaceholderTimerRef.current = null; } updateImageGenerationPhase("idle"); } // 🆕 将消息添加到 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]; }); 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(); } 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 { 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())); }); 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 ( 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" || model?.providerType === "ruxa-image") modelType = "alibaba-image"; console.log(`[Chat] 🚀 记录生成任务: modelType=${modelType}, modelId=${currentModelId}`); startGeneration(currentModelId, modelType); if (modelType === "gemini" || modelType === "volcano-vision" || modelType === "alibaba-image") { updateImageGenerationPhase("queued"); } } }, [status, currentModelId, startGeneration, updateImageGenerationPhase]); 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 = async (annotatedImageUrl: string) => { // annotatedImageUrl 是 base64 data URL 格式,需要先上传到服务器获取 URL const fileName = `annotated-${Date.now()}.png`; try { // 如果是 base64 格式,先上传到服务器 if (annotatedImageUrl.startsWith("data:")) { toast.info("正在上传标注图片..."); // 将 base64 转换为 Blob const response = await fetch(annotatedImageUrl); const blob = await response.blob(); // 创建 FormData 上传 const formData = new FormData(); formData.append("file", blob, fileName); const uploadResponse = await fetch("/api/files/upload", { method: "POST", body: formData, }); if (!uploadResponse.ok) { throw new Error("上传失败"); } const uploadResult = await uploadResponse.json(); setAttachments((prev) => [ ...prev, { url: uploadResult.url, name: fileName, contentType: "image/png", }, ]); toast.success("标注图片已添加"); } else { // 如果已经是 URL,直接使用 setAttachments((prev) => [ ...prev, { url: annotatedImageUrl, name: fileName, contentType: "image/png", }, ]); } // 如果输入框为空,自动填入提示词 setInput((prev) => prev ? prev : "请根据我的标注修改这张图片:"); } catch (error) { console.error("上传标注图片失败:", error); toast.error("上传标注图片失败,请重试"); } }; // 处理图片变清晰 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 handleImageWatermarkRemove = async (imageUrl: string) => { // 防止重复点击 if (isProcessingEnhance || isProcessingRmbg || isProcessingExtend || isProcessingWatermarkRemove) { toast.info("正在处理中,请稍候..."); return; } setIsProcessingWatermarkRemove(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 { // 调用去水印API const watermarkRemoveResponse = await fetch("/api/watermark-remove", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ image_url: imageUrl, }), }); if (!watermarkRemoveResponse.ok) { const error = await watermarkRemoveResponse.json(); throw new Error(error.error || "去水印处理失败"); } const result = await watermarkRemoveResponse.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 }], createdAt: new Date().toISOString(), } as ChatMessage; setMessages((prev) => { const filtered = prev.filter((msg) => msg.id !== messageId); return [...filtered, finalMessage]; }); // 保存消息到数据库 try { const saveResponse = 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 }], }, }), }); if (!saveResponse.ok) { console.error("[Chat] Failed to save message:", await saveResponse.text()); } } 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 { setIsProcessingWatermarkRemove(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 = "/"; }} > 立即启用 ); }