"use client"; import { useChat } from "@ai-sdk/react"; import { DefaultChatTransport } from "ai"; import { useRouter, useSearchParams } from "next/navigation"; import { useEffect, 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); // 🆕 当检测到恢复的 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 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) { setImageParams(JSON.parse(saved)); } } 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 { // dataPart.data 是 JSON 字符串 const messageData = typeof dataPart.data === 'string' ? JSON.parse(dataPart.data) : dataPart.data; console.log("[Chat] Received appendMessage:", messageData); // 🆕 收到消息,清除持久化状态 completeGeneration(); setMessages((prevMessages) => { if (prevMessages.some(m => m.id === messageData.id)) { return prevMessages; } // 构造 ChatMessage 对象 // 注意:这里需要确保符合 ChatMessage 类型定义 const newMessage: any = { id: messageData.id, role: messageData.role, content: messageData.parts?.[0]?.text || "", createdAt: new Date(), parts: messageData.parts || [], attachments: [], }; return [...prevMessages, newMessage]; }); } catch (e) { console.error("[Chat] Failed to parse data-appendMessage:", e); } } }, onFinish: () => { mutate(unstable_serialize(getChatHistoryPaginationKey)); // 🆕 生成完成,清除持久化状态 completeGeneration(); }, 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); } } }, }); // 调试:监控聊天状态变化 & 持久化生成状态 // 使用 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"; if (model?.providerType === "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"; 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 { 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) => { const messageId = generateUUID(); // 创建临时加载消息 const loadingMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: "正在变清晰..." }], createdAt: new Date().toISOString(), } as ChatMessage; setMessages((prev) => [...prev, loadingMessage]); 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)) ); // 保存消息到数据库 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)); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); }; reader.readAsDataURL(blob); } catch (error) { console.error("[变清晰] 错误:", error); toast.error(error instanceof Error ? error.message : "变清晰失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); } }; // 处理图片抠图 const handleImageRmbg = async (imageUrl: string) => { const messageId = generateUUID(); // 创建临时加载消息 const loadingMessage: ChatMessage = { id: messageId, role: "assistant", parts: [{ type: "text", text: "正在抠图..." }], createdAt: new Date().toISOString(), } as ChatMessage; setMessages((prev) => [...prev, loadingMessage]); 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)) ); // 保存消息到数据库 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)); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); }; reader.readAsDataURL(blob); } catch (error) { console.error("[抠图] 错误:", error); toast.error(error instanceof Error ? error.message : "抠图失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); } }; // 处理图像拓展 const handleImageExtend = async (imageUrl: string) => { 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]); 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)) ); // 保存消息到数据库 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)); } }; reader.onerror = () => { toast.error("图片读取失败"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); }; reader.readAsDataURL(blob); } catch (error) { console.error("[图像拓展] 错误:", error); toast.error(error instanceof Error ? error.message : "图像拓展失败,请稍后重试"); setMessages((prev) => prev.filter((msg) => msg.id !== messageId)); } }; useAutoResume({ autoResume, initialMessages, resumeStream, setMessages, }); 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 = "/"; }} > 立即启用 ); }