From 59a81f3c0ed67fe1c094961f9d2cd2e8ef67ec44 Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 17:12:30 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=BB=E5=B8=83=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E8=8A=82=E7=82=B9=E6=B7=BB=E5=8A=A0=E5=9B=BE=E7=89=87=E6=A0=87?= =?UTF-8?q?=E6=B3=A8=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Cursor --- components/canvas/CanvasChatPanel.tsx | 14 +++++++- components/canvas/nodes/ImageNode.tsx | 50 ++++++++++++++++++++++++++- lib/ai/prompt-optimizer.ts | 12 +++++-- types/canvas/index.ts | 1 + 4 files changed, 73 insertions(+), 4 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 332cc4940..e3fe6250f 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -456,6 +456,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } } + // 🆕 检查是否有标注的图片 + const annotatedNodes = selectedImageNodes.filter(node => (node.data as any)?.hasAnnotations); + const hasAnnotatedImages = annotatedNodes.length > 0; + // 添加文件部分(作为 file 类型的 part) effectiveAttachments.forEach((a) => { parts.push({ @@ -463,11 +467,19 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF mediaType: a.contentType, name: a.name, url: a.url, + hasAnnotations: hasAnnotatedImages, // 🆕 标记图片是否有标注 }); }); // 添加文本部分 - const textContent = input.trim() || (effectiveAttachments.length > 0 ? "请处理这张图片" : ""); + let textContent = input.trim() || (effectiveAttachments.length > 0 ? "请处理这张图片" : ""); + + // 🆕 如果有标注的图片,在文本中添加提示 + if (hasAnnotatedImages && textContent) { + textContent = `【注意:图片中有标注(箭头、圆圈、文字等)标记的区域是需要重点关注或修改的部分,请根据标注进行处理】\n${textContent}`; + console.log('[CanvasChat] 检测到标注图片,已添加标注提示'); + } + if (textContent) { parts.push({ type: "text" as const, text: textContent }); } diff --git a/components/canvas/nodes/ImageNode.tsx b/components/canvas/nodes/ImageNode.tsx index c595f8e09..54666eede 100644 --- a/components/canvas/nodes/ImageNode.tsx +++ b/components/canvas/nodes/ImageNode.tsx @@ -2,7 +2,7 @@ import React, { useState, useRef, useEffect } from "react"; import { createPortal } from "react-dom"; -import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw } from "lucide-react"; +import { ImagePlus, Loader2, AlertCircle, MoreVertical, Scissors, Sparkles, Expand, Maximize2, Move3D, Layers, Download, Trash2, RefreshCw, Pencil } from "lucide-react"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -27,6 +27,7 @@ import type { CanvasNode, ImageNodeData } from "@/types/canvas"; import { cn } from "@/lib/utils"; import { AngleEditDialog } from "@/components/angle-edit-dialog"; import { SceneFusionDialog } from "@/components/scene-fusion-dialog"; +import { ImageAnnotator } from "@/components/image-annotator"; interface ImageNodeProps { node: CanvasNode; @@ -43,6 +44,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { const [resizeWidth, setResizeWidth] = useState(1024); const [resizeHeight, setResizeHeight] = useState(1024); const [isUploading, setIsUploading] = useState(false); + const [annotatorOpen, setAnnotatorOpen] = useState(false); // 文件输入的 ref const fileInputRef = useRef(null); @@ -136,6 +138,7 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 'resize': '尺寸调整', 'angle': '角度编辑', 'scene-fusion': '详情替换', + 'annotate': '图片标注', }; return names[action] || action; }; @@ -160,6 +163,10 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { setResizeDialogOpen(true); return; } + if (action === 'annotate') { + setAnnotatorOpen(true); + return; + } const actionName = getActionName(action); @@ -263,6 +270,26 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { }); }; + // 图片标注完成回调 + const handleAnnotateComplete = (annotatedImageUrl: string) => { + setAnnotatorOpen(false); + + // 更新当前节点,标记为已标注图片 + // 标注后的图片会带有视觉标记(箭头、圆圈、文字等) + // 这些标记指示了用户希望 AI 重点关注/修改的区域 + updateNode(node.id, { + data: { + ...data, + src: annotatedImageUrl, + hasAnnotations: true, // 🆕 标记为已标注 + originalSrc: data.originalSrc || data.src, // 保留原始图片 + prompt: data.prompt ? `${data.prompt} (已标注)` : '已标注图片', + }, + }); + + toast.success('标注已保存!发送对话时将重点处理标注区域'); + }; + // 尺寸调整执行 const handleResize = async () => { if (!data.src) return; @@ -617,6 +644,18 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { 调整 + {/* 图片标注 */} + + {/* 更多操作 */} @@ -647,6 +686,15 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) { , document.body )} + + {/* 图片标注器 */} + {annotatorOpen && data.src && ( + setAnnotatorOpen(false)} + /> + )} ); } diff --git a/lib/ai/prompt-optimizer.ts b/lib/ai/prompt-optimizer.ts index 7c7ede747..6ff84b59a 100644 --- a/lib/ai/prompt-optimizer.ts +++ b/lib/ai/prompt-optimizer.ts @@ -57,6 +57,7 @@ const EDIT_TEMPLATES: Record< negative: string; extractors: Record; defaults: Record; + useOriginalPrompt?: boolean; // 🆕 是否使用原始提示词 } > = { remove_background: { @@ -178,6 +179,13 @@ const LLM_OPTIMIZER_SYSTEM_PROMPT = `你是一个专业的AI图片编辑提示 3. **细节要求**:包含颜色、材质、光影、位置等具体参数 4. **自然过渡**:修改区域与周围环境自然融合 +## 图片标注处理(重要!) +如果用户消息中提到"图片中有标注"或"标注区域",说明用户已经在图片上画了标记(箭头、圆圈、文字等)来指示需要修改的区域: +- **优先处理标注区域**:将标注标记的区域作为主要编辑目标 +- **理解标注意图**:箭头指向的位置、圆圈圈住的区域、文字说明的内容都是用户想要修改的部分 +- **强调标注**:在提示词中明确提到"按照图片中的标注/标记指示进行修改" +- **保护非标注区域**:标注以外的区域应保持完全不变 + ## 输出格式(严格JSON) { "optimizedPrompt": "精准的图片编辑指令", @@ -403,7 +411,7 @@ export function optimizePrompt(params: { friendlyMessage = friendlyTemplate.template; // 填充友好消息中的变量 - for (const [key, regex] of Object.entries(template.extractors)) { + for (const [key, regex] of Object.entries(template.extractors) as [string, RegExp][]) { const match = originalPrompt.match(regex); let value = match ? match[1].trim() : template.defaults[key] || ""; value = value.replace(/[,。!?,.!?]/g, "").trim(); @@ -414,7 +422,7 @@ export function optimizePrompt(params: { } // 填充剩余的默认值 - for (const [key, defaultValue] of Object.entries(template.defaults)) { + for (const [key, defaultValue] of Object.entries(template.defaults) as [string, string][]) { friendlyMessage = friendlyMessage.replace(`{${key}}`, defaultValue); } } diff --git a/types/canvas/index.ts b/types/canvas/index.ts index 4818e585b..03448b3a1 100644 --- a/types/canvas/index.ts +++ b/types/canvas/index.ts @@ -40,6 +40,7 @@ export interface ImageNodeData { // 图片编辑相关 originalSrc?: string; editHistory?: ImageEditAction[]; + hasAnnotations?: boolean; // 🆕 标记图片是否有用户标注 } // 视频节点数据