From 5e642165df8bb4b220d2263650175d7e5c2f914f Mon Sep 17 00:00:00 2001 From: Your Name Date: Sun, 1 Feb 2026 12:41:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E4=B8=8A=E4=BC=A0=E5=9B=BE=E7=89=87?= =?UTF-8?q?=E8=87=AA=E5=8A=A8=E8=AF=86=E5=88=AB=E5=B9=B6=E5=91=BD=E5=90=8D?= =?UTF-8?q?=E7=94=BB=E5=B8=83?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 使用千问VL模型识别图片主体作为画布名称 2. 第一张上传的图片自动设为画布缩略图 3. 画布列表显示缩略图预览 Co-authored-by: Cursor --- components/canvas/CanvasChatPanel.tsx | 51 +++++++++++++++++++++++++-- 1 file changed, 49 insertions(+), 2 deletions(-) diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 8d62c87..f974dc3 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -256,14 +256,20 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF messagesEndRef.current?.scrollIntoView({ behavior: "smooth" }); }, [messages]); + // 标记是否已经为当前画布设置过标题(避免重复调用) + const hasSetCanvasTitle = useRef(false); + // 处理发送 - const handleSend = useCallback(() => { + const handleSend = useCallback(async () => { if (!input.trim() && attachments.length === 0) return; if (!selectedModelId) { toast.error("请选择一个模型"); return; } + // 获取第一张图片的 URL(用于自动命名和缩略图) + const firstImageAttachment = attachments.find(a => a.contentType?.startsWith('image/')); + // 构建 parts 数组:文件在前,文本在后(与 multimodal-input 一致) const parts: any[] = []; @@ -313,9 +319,50 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF toast.success(`已将 ${attachments.length} 张图片添加到画布`); } + // 如果是第一次上传图片,自动识别并设置画布标题和缩略图 + if (firstImageAttachment && !hasSetCanvasTitle.current) { + hasSetCanvasTitle.current = true; + + // 异步调用 AI 识别图片主体并更新画布 + (async () => { + try { + // 调用 auto-title API 识别图片主体 + const response = await fetch('/api/canvas/auto-title', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ imageUrl: firstImageAttachment.url }), + }); + + if (response.ok) { + const data = await response.json(); + const title = data.title || '未命名画布'; + + // 提取纯 UUID(去掉 canvas- 前缀) + const rawId = chatId.startsWith('canvas-') + ? chatId.replace('canvas-', '') + : chatId; + + // 更新画布标题和缩略图 + await fetch(`/api/canvas/${rawId}`, { + method: 'PUT', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + title: title, + thumbnail: firstImageAttachment.url, + }), + }); + + console.log('[CanvasChat] 画布已自动命名为:', title); + } + } catch (error) { + console.error('[CanvasChat] 自动命名失败:', error); + } + })(); + } + setInput(""); setAttachments([]); - }, [input, attachments, selectedModelId, sendMessage, addNode, nodes.length]); + }, [input, attachments, selectedModelId, sendMessage, addNode, nodes.length, chatId]); // 处理文件选择 const handleFileSelect = async (e: React.ChangeEvent) => {