From 2ea63d978fcd2459537ee6db424674e2d347175c Mon Sep 17 00:00:00 2001 From: Your Name Date: Thu, 5 Feb 2026 20:53:01 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E8=87=AA=E5=8A=A8=E6=A8=A1?= =?UTF-8?q?=E5=BC=8F=E4=B8=8A=E4=B8=8B=E6=96=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(chat)/api/chat/route.ts | 5 ++ app/(chat)/api/chat/schema.ts | 1 + components/canvas/CanvasChatPanel.tsx | 53 +++++++++++- components/scene-fusion-view.tsx | 113 +++++++------------------- lib/ai/prompt-optimizer.ts | 10 ++- lib/ai/smart-analyzer.ts | 16 +++- 6 files changed, 107 insertions(+), 91 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index 01713f2..b58e12e 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -337,12 +337,14 @@ export async function POST(request: Request) { selectedChatModel, selectedVisibilityType, hiddenPrompt, + autoContext, }: { id: string; message: ChatMessage; selectedChatModel: ChatModel["id"]; selectedVisibilityType: VisibilityType; hiddenPrompt?: string; + autoContext?: string; } = requestBody; let finalModelId = selectedChatModel; @@ -367,6 +369,7 @@ export async function POST(request: Request) { .filter((p): p is { type: "text"; text: string } => p.type === "text") .map((p) => p.text) .join(" "); + const autoContextText = typeof autoContext === "string" ? autoContext.trim() : ""; // @ts-ignore - 检查各种可能的图片类型 let hasImage = lastUserMessage.parts.some( @@ -466,6 +469,7 @@ export async function POST(request: Request) { imageUrls, imageFromHistory: smartAutoHistoryImages.length > 0, // 🆕 标识图片是否来自历史 historyMessages, // 🆕 传入历史对话用于上下文理解 + context: autoContextText || undefined, }); console.log("[Smart Auto Mode] Analysis result:", { @@ -489,6 +493,7 @@ export async function POST(request: Request) { intent: analysisResult.intent, subIntent: analysisResult.subIntent, historyMessages, // 🆕 传入历史对话上下文 + context: autoContextText || undefined, }); console.log("[Smart Auto Mode] ✨ Prompt optimized (LLM):", { diff --git a/app/(chat)/api/chat/schema.ts b/app/(chat)/api/chat/schema.ts index 28e3cff..79b97fd 100644 --- a/app/(chat)/api/chat/schema.ts +++ b/app/(chat)/api/chat/schema.ts @@ -50,6 +50,7 @@ export const postRequestBodySchema = z.object({ parts: z.array(partSchema), }), hiddenPrompt: z.string().max(4000).optional(), + autoContext: z.string().max(4000).optional(), // 支持预定义模型名称或数据库中的模型 UUID selectedChatModel: z.string().min(1).max(128), selectedVisibilityType: z.enum(["public", "private"]), diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 8e5ba3a..257dbc5 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -143,6 +143,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const fileInputRef = useRef(null); const textareaRef = useRef(null); const hiddenPromptRef = useRef(null); + const autoContextRef = useRef(null); // 🆕 标签悬停预览状态 const [tagPreview, setTagPreview] = useState(null); @@ -557,7 +558,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF lastMessage: request.messages.at(-1), }); const hiddenPrompt = hiddenPromptRef.current || undefined; + const autoContext = autoContextRef.current || undefined; hiddenPromptRef.current = null; + autoContextRef.current = null; return { body: { id: rawId, @@ -567,6 +570,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF videoParams: undefined, // 必须传递,即使是 undefined imageParams: imageParamsRef.current || undefined, // 传递图片参数 hiddenPrompt, + autoContext, ...request.body, }, }; @@ -765,6 +769,50 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF return urls; }, [selectedImageNodes, nodes, messages]); + // 🆕 智能自动模式上下文(画布图片布局) + const buildAutoContext = useCallback(() => { + const imageNodes = nodes.filter(n => n.type === 'image' && (n.data as any)?.src); + if (imageNodes.length === 0) return ""; + + const MAX_CONTEXT_IMAGES = 6; + const xs = imageNodes.map(n => n.position.x); + const ys = imageNodes.map(n => n.position.y); + const minX = Math.min(...xs); + const maxX = Math.max(...xs); + const minY = Math.min(...ys); + const maxY = Math.max(...ys); + const spanX = Math.max(1, maxX - minX); + const spanY = Math.max(1, maxY - minY); + + const positionLabel = (node: (typeof imageNodes)[number]) => { + const relX = (node.position.x - minX) / spanX; + const relY = (node.position.y - minY) / spanY; + const horizontal = relX <= 0.33 ? "左" : relX >= 0.66 ? "右" : "中"; + const vertical = relY <= 0.33 ? "上" : relY >= 0.66 ? "下" : "中"; + if (horizontal === "中" && vertical === "中") return "中间"; + return `${vertical}${horizontal}`; + }; + + const selectedNodes = selectedImageNodes.length > 0 ? selectedImageNodes : []; + const contextNodes = (selectedNodes.length > 0 ? selectedNodes : imageNodes.slice(-MAX_CONTEXT_IMAGES)).slice(0, MAX_CONTEXT_IMAGES); + + const lines = [ + `画布图片总数: ${imageNodes.length}`, + selectedNodes.length > 0 + ? `用户已选中 ${selectedNodes.length} 张图片,优先处理这些` + : `用户未选中图片,默认使用最近 ${contextNodes.length} 张`, + ...contextNodes.map((node, index) => { + const label = positionLabel(node); + const size = node.size ? `${Math.round(node.size.width)}x${Math.round(node.size.height)}` : "未知尺寸"; + const prompt = (node.data as any)?.prompt; + return `图片${index + 1}: ${label},尺寸 ${size}${prompt ? `,描述:${String(prompt).slice(0, 60)}` : ""}`; + }), + "若用户提到左/右/上/下/中等方位,请匹配对应位置的图片。", + ]; + + return lines.join("\n"); + }, [nodes, selectedImageNodes]); + // 处理发送(图片已在上传时添加到画布,这里只发送消息) const handleSend = useCallback(async () => { if (!input.trim() && attachments.length === 0) return; @@ -840,6 +888,9 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF const hiddenPrompt = hiddenSegments.join('\n\n').trim(); hiddenPromptRef.current = hiddenPrompt || null; + const autoContext = selectedModelId === "auto" ? buildAutoContext() : ""; + autoContextRef.current = autoContext ? autoContext : null; + if (textContent) { parts.push({ type: "text" as const, text: textContent }); } @@ -863,7 +914,7 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF editableDiv.innerHTML = ''; console.log('[CanvasChat] 已清空输入框和选中状态'); } - }, [input, attachments, selectedModelId, sendMessage, getRecentImageUrls, clearSelection]); + }, [input, attachments, selectedModelId, sendMessage, getRecentImageUrls, clearSelection, buildAutoContext]); // 处理文件选择 - 上传后立即添加到画布 const handleFileSelect = async (e: React.ChangeEvent) => { diff --git a/components/scene-fusion-view.tsx b/components/scene-fusion-view.tsx index 02e8200..249a458 100644 --- a/components/scene-fusion-view.tsx +++ b/components/scene-fusion-view.tsx @@ -92,31 +92,29 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam const [productName, setProductName] = useState(initialProductName || ""); // 产品名称(如"沙发") const [scenes, setScenes] = useState([]); const [isProcessing, setIsProcessing] = useState(false); - const [isProductUploading, setIsProductUploading] = useState(false); const [aspectRatio, setAspectRatio] = useState("auto"); // 图片比例 const [imageSize, setImageSize] = useState("2K"); // 图片分辨率 // 文件上传 Input 引用 const productInputRef = useRef(null); const sceneInputRef = useRef(null); - const productUploadIdRef = useRef(0); + const productPreviewObjectUrlRef = useRef(null); // 如果外部传入了初始产品图,则在首次挂载或传入值变化时自动带入 - const updateProductPreview = (previewUrl: string | null) => { + const updateProductPreview = (previewUrl: string | null, isObjectUrl = false) => { + if (productPreviewObjectUrlRef.current) { + URL.revokeObjectURL(productPreviewObjectUrlRef.current); + productPreviewObjectUrlRef.current = null; + } setProductImagePreview(previewUrl); + if (isObjectUrl && previewUrl) { + productPreviewObjectUrlRef.current = previewUrl; + } }; const clearProductImage = () => { setProductImage(null); updateProductPreview(null); - setIsProductUploading(false); - productUploadIdRef.current += 1; - }; - - const handleProductPreviewError = () => { - if (productImage && productImagePreview !== productImage) { - updateProductPreview(productImage); - } }; useEffect(() => { @@ -126,37 +124,18 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam } }, [initialProductImage]); - // 处理产品图上传 - const uploadProductImageFile = async (file: File): Promise => { - const formData = new FormData(); - formData.append("file", file); - const response = await fetch("/api/files/upload", { - method: "POST", - body: formData, - }); - - if (!response.ok) { - let errorMessage = "上传失败"; - try { - const errorData = await response.json(); - errorMessage = errorData?.error || errorData?.message || errorMessage; - } catch { - // ignore + useEffect(() => { + return () => { + if (productPreviewObjectUrlRef.current) { + URL.revokeObjectURL(productPreviewObjectUrlRef.current); + productPreviewObjectUrlRef.current = null; } - throw new Error(errorMessage); - } + }; + }, []); - const result = await response.json(); - if (!result?.url) { - throw new Error("上传失败"); - } - - return result.url as string; - }; - - const handleProductUpload = async (e: React.ChangeEvent) => { - const input = e.target; - const file = input.files?.[0]; + // 处理产品图上传 + const handleProductUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; if (!file) return; if (file.size > 10 * 1024 * 1024) { @@ -165,16 +144,13 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam } setProductImage(null); - updateProductPreview(null); - setIsProductUploading(true); - const uploadId = ++productUploadIdRef.current; + const previewUrl = URL.createObjectURL(file); + updateProductPreview(previewUrl, true); const reader = new FileReader(); reader.onload = (e) => { - if (uploadId !== productUploadIdRef.current) return; const result = e.target?.result; if (typeof result === "string") { - updateProductPreview(result); setProductImage(result); } else { toast.error("图片读取失败,请重试"); @@ -182,28 +158,12 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam } }; reader.onerror = () => { - if (uploadId !== productUploadIdRef.current) return; toast.error("图片读取失败,请重试"); clearProductImage(); }; reader.readAsDataURL(file); - - try { - toast.info("正在上传产品图..."); - const uploadedUrl = await uploadProductImageFile(file); - if (uploadId === productUploadIdRef.current) { - setProductImage(uploadedUrl); - toast.success("产品图上传成功"); - } - } catch (error) { - toast.error(error instanceof Error ? error.message : "产品图上传失败"); - } finally { - if (uploadId === productUploadIdRef.current) { - setIsProductUploading(false); - } - } // 重置 input,允许重复选择同一文件 - input.value = ""; + e.target.value = ""; }; // 处理场景图上传 @@ -672,19 +632,11 @@ export function SceneFusionView({ onBack, initialProductImage, initialProductNam
-
- {isProductUploading && ( - - - 上传中... - - )} - {(productImagePreview || productImage) && ( - - )} -
+ {(productImagePreview || productImage) && ( + + )}
{(productImagePreview || productImage) ? ( -
- 产品图 +
+ 产品图