diff --git a/app/api/messages/route.ts b/app/api/messages/route.ts new file mode 100644 index 0000000..c47a8fb --- /dev/null +++ b/app/api/messages/route.ts @@ -0,0 +1,55 @@ +import { NextRequest, NextResponse } from "next/server"; +import { auth } from "@/app/(auth)/auth"; +import { saveMessages } from "@/lib/db/queries"; +import type { DBMessage } from "@/lib/db/schema"; + +/** + * 保存消息到数据库的API + * 用于图片处理等场景中保存助手回复消息 + */ +export async function POST(request: NextRequest) { + try { + const session = await auth(); + + if (!session?.user?.id) { + return NextResponse.json({ error: "Unauthorized" }, { status: 401 }); + } + + const body = await request.json(); + const { chatId, message } = body; + + if (!chatId || !message) { + return NextResponse.json( + { error: "Missing required fields: chatId and message" }, + { status: 400 } + ); + } + + // 构造DBMessage对象 + const dbMessage: DBMessage = { + id: message.id, + chatId, + role: message.role || "assistant", + parts: message.parts || [], + attachments: message.attachments || [], + createdAt: message.createdAt ? new Date(message.createdAt) : new Date(), + videoStorageStatus: null, + temporaryVideoUrl: null, + permanentVideoUrl: null, + archivedAt: null, + videoMetadata: null, + }; + + // 保存消息到数据库 + await saveMessages({ messages: [dbMessage] }); + + return NextResponse.json({ success: true }); + } catch (error) { + console.error("[Messages API] Error:", error); + return NextResponse.json( + { error: "Failed to save message" }, + { status: 500 } + ); + } +} + diff --git a/components/chat.tsx b/components/chat.tsx index 56bd278..07493b3 100644 --- a/components/chat.tsx +++ b/components/chat.tsx @@ -32,7 +32,7 @@ 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 "@/components/toast"; +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"; @@ -317,10 +317,7 @@ export function Chat({ ) { setShowCreditCardAlert(true); } else { - toast({ - type: "error", - description: error.message, - }); + toast.error(error.message); } } }, @@ -448,6 +445,214 @@ export function Chat({ 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 }], + 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: finalMessage, + }), + }); + } 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 }], + 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, @@ -477,6 +682,8 @@ export function Chat({ votes={votes} onImageEdit={handleImageEdit} onDrawingComplete={handleDrawingComplete} + onImageEnhance={handleImageEnhance} + onImageRmbg={handleImageRmbg} pendingGeneration={pendingGeneration} imageCount={imageParams?.imageCount || 1} /> diff --git a/components/elements/response.tsx b/components/elements/response.tsx index 803dd9d..b60f698 100644 --- a/components/elements/response.tsx +++ b/components/elements/response.tsx @@ -9,10 +9,12 @@ import { VideoPreview } from "@/components/video-preview"; type ResponseProps = ComponentProps & { onImageEdit?: (imageUrl: string) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; + onImageEnhance?: (imageUrl: string) => void; + onImageRmbg?: (imageUrl: string) => void; }; export const Response = memo( - ({ className, onImageEdit, onDrawingComplete, ...props }: ResponseProps) => ( + ({ className, onImageEdit, onDrawingComplete, onImageEnhance, onImageRmbg, ...props }: ResponseProps) => ( *:first-child]:mt-0 [&>*:last-child]:mb-0 [&_code]:whitespace-pre-wrap [&_code]:break-words [&_pre]:max-w-full [&_pre]:overflow-x-auto", @@ -23,7 +25,14 @@ export const Response = memo( if (!src) { return null; } - return ; + return ; }, video: ({ src, alt }: { src?: string; alt?: string }) => { if (!src) { @@ -36,7 +45,11 @@ export const Response = memo( /> ), (prevProps: ResponseProps, nextProps: ResponseProps) => - prevProps.children === nextProps.children && prevProps.onImageEdit === nextProps.onImageEdit && prevProps.onDrawingComplete === nextProps.onDrawingComplete + prevProps.children === nextProps.children && + prevProps.onImageEdit === nextProps.onImageEdit && + prevProps.onDrawingComplete === nextProps.onDrawingComplete && + prevProps.onImageEnhance === nextProps.onImageEnhance && + prevProps.onImageRmbg === nextProps.onImageRmbg ); Response.displayName = "Response"; diff --git a/components/image-preview.tsx b/components/image-preview.tsx index f822cc4..2145c5f 100644 --- a/components/image-preview.tsx +++ b/components/image-preview.tsx @@ -1,7 +1,7 @@ "use client"; import React, { useState } from "react"; -import { X, Download, Edit, Target, Paintbrush } from "lucide-react"; +import { X, Download, Edit, Target, Paintbrush, Sparkles, Scissors } from "lucide-react"; import { toast } from "sonner"; import { ImageAnnotator } from "./image-annotator"; @@ -11,6 +11,8 @@ export function ImagePreview({ onImageEdit, onAnnotate, onDrawingComplete, + onImageEnhance, + onImageRmbg, imageIndex }: { src: string; @@ -22,6 +24,8 @@ export function ImagePreview({ formattedText: string; }) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; + onImageEnhance?: (imageUrl: string) => void; + onImageRmbg?: (imageUrl: string) => void; imageIndex?: number; }) { const [isOpen, setIsOpen] = useState(false); @@ -199,6 +203,40 @@ export function ImagePreview({ onClick={() => setIsOpen(true)} /> + {/* 左上角:变清晰和抠图按钮 */} + {(onImageEnhance || onImageRmbg) && ( +
+ {onImageEnhance && ( + + )} + {onImageRmbg && ( + + )} +
+ )} + {/* 悬停时显示的操作按钮 */}
{/* 画图按钮 */} diff --git a/components/image-processing-loading.tsx b/components/image-processing-loading.tsx new file mode 100644 index 0000000..678ecbc --- /dev/null +++ b/components/image-processing-loading.tsx @@ -0,0 +1,158 @@ +"use client"; + +import { memo } from "react"; +import { SparklesIcon } from "./icons"; +import { Scissors } from "lucide-react"; + +interface ImageProcessingLoadingProps { + type: "enhance" | "rmbg"; // 处理类型:变清晰或抠图 +} + +/** + * 图片处理加载占位符组件 + * 显示类似 Gemini 生图的动态卡片效果 + */ +export const ImageProcessingLoading = memo(({ type }: ImageProcessingLoadingProps) => { + const isEnhance = type === "enhance"; + const color = isEnhance ? "blue" : "green"; + const text = isEnhance ? "正在变清晰" : "正在抠图"; + const icon = isEnhance ? SparklesIcon : Scissors; + const Icon = icon; + + // 根据类型设置颜色类名 + const colorClasses = isEnhance + ? { + ping: "bg-blue-500/20", + pulse: "bg-blue-500/10", + ring: "ring-blue-500/30", + text: "text-blue-500", + dot: "bg-blue-500", + gradientFrom: "from-blue-400", + gradientFromOverlay: "from-blue-500/40", + gradientToOverlay: "to-blue-500/30", + } + : { + ping: "bg-green-500/20", + pulse: "bg-green-500/10", + ring: "ring-green-500/30", + text: "text-green-500", + dot: "bg-green-500", + gradientFrom: "from-green-400", + gradientFromOverlay: "from-green-500/40", + gradientToOverlay: "to-green-500/30", + }; + + return ( +
+
+ {/* 头像 */} +
+
+
+
+
+ +
+
+
+ +
+ {/* 文本提示 */} +
+ + {text} + + + + + + +
+ + {/* 图片占位符 - 170x200px */} +
+ {/* 底层渐变背景 */} +
+ + {/* 动态模糊叠加层 1 */} +
+ + {/* 动态模糊叠加层 2 */} +
+ + {/* 噪点纹理效果 */} +
+ + {/* 模糊叠加 */} +
+ + {/* 中心图标 */} +
+
+ {/* 外圈光晕 */} +
+ {/* 图标 */} + +
+
+ + {/* 流光效果 */} +
+
+ + {/* 提示文字 */} + + 正在处理图片,请稍候... + +
+
+ + {/* 流光动画样式 */} + +
+ ); +}); + +ImageProcessingLoading.displayName = "ImageProcessingLoading"; + diff --git a/components/message.tsx b/components/message.tsx index fc8884a..8ceae80 100644 --- a/components/message.tsx +++ b/components/message.tsx @@ -23,6 +23,7 @@ import { MessageEditor } from "./message-editor"; import { MessageReasoning } from "./message-reasoning"; import { PreviewAttachment } from "./preview-attachment"; import { Weather } from "./weather"; +import { ImageProcessingLoading } from "./image-processing-loading"; /** * 解析文本中的标注内容,返回文本片段数组 @@ -147,6 +148,8 @@ const PurePreviewMessage = ({ requiresScrollPadding: _requiresScrollPadding, onImageEdit, onDrawingComplete, + onImageEnhance, + onImageRmbg, }: { chatId: string; message: ChatMessage; @@ -159,6 +162,8 @@ const PurePreviewMessage = ({ requiresScrollPadding: boolean; onImageEdit?: (imageUrl: string) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; + onImageEnhance?: (imageUrl: string) => void; + onImageRmbg?: (imageUrl: string) => void; }) => { const [mode, setMode] = useState<"view" | "edit">("view"); @@ -235,6 +240,17 @@ const PurePreviewMessage = ({ if (type === "text") { if (mode === "view") { + // 检查是否是图片处理的加载消息 + if (part.text === "正在变清晰..." || part.text === "正在抠图...") { + const isEnhance = part.text === "正在变清晰..."; + return ( + + ); + } + // 应用视频内容过滤 const filteredText = filterVideoContent(part.text); const textParts = parseAnnotations(filteredText); @@ -267,7 +283,13 @@ const PurePreviewMessage = ({ ); } return ( - + {sanitizeText(part.content)} ); diff --git a/components/messages.tsx b/components/messages.tsx index 9c642b5..04a139a 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -37,6 +37,8 @@ type MessagesProps = { selectedModelId: string; onImageEdit?: (imageUrl: string) => void; onDrawingComplete?: (annotatedImageUrl: string) => void; + onImageEnhance?: (imageUrl: string) => void; + onImageRmbg?: (imageUrl: string) => void; pendingGeneration?: PendingGeneration | null; // 🆕 持久化的生成状态 imageCount?: number; // 🆕 用户选择的图片数量 }; @@ -53,6 +55,8 @@ function PureMessages({ selectedModelId, onImageEdit, onDrawingComplete, + onImageEnhance, + onImageRmbg, pendingGeneration, imageCount = 1, }: MessagesProps) { @@ -220,6 +224,8 @@ function PureMessages({ } onImageEdit={onImageEdit} onDrawingComplete={onDrawingComplete} + onImageEnhance={onImageEnhance} + onImageRmbg={onImageRmbg} /> ))}