From 1556826c1b763f0c9421db784504a63a1b782264 Mon Sep 17 00:00:00 2001 From: Your Name Date: Mon, 9 Feb 2026 00:32:11 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E7=94=BB=E5=B8=83=E9=A2=84?= =?UTF-8?q?=E8=A7=88=E6=94=BE=E5=A4=A7=E4=BA=A4=E4=BA=92=E4=B8=8E=E8=A7=A6?= =?UTF-8?q?=E6=8E=A7=E6=9D=BF=E6=A8=AA=E5=90=91=E6=BB=91=E5=8A=A8=E7=BF=BB?= =?UTF-8?q?=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(chat)/api/chat/route.ts | 70 +++++++++++++++++++-------- app/canvas/[id]/page.tsx | 2 +- components/canvas/CanvasChatPanel.tsx | 53 +++++++++++++++++++- components/canvas/CanvasWorkspace.tsx | 17 ++++++- 4 files changed, 118 insertions(+), 24 deletions(-) diff --git a/app/(chat)/api/chat/route.ts b/app/(chat)/api/chat/route.ts index a3ffbea..9762ae9 100644 --- a/app/(chat)/api/chat/route.ts +++ b/app/(chat)/api/chat/route.ts @@ -488,34 +488,64 @@ async function normalizeAssistantImageMarkdown( if (!text || !text.includes("data:image")) return text; const markdownDataUrlRegex = /!\[([^\]]*)\]\s*\(\s*(data:image\/[a-z0-9+.-]+;base64,[^)]+)\s*\)/gim; - const matches = [...text.matchAll(markdownDataUrlRegex)]; - if (matches.length === 0) { - return text; - } + const markdownMatches = [...text.matchAll(markdownDataUrlRegex)]; + let normalizedText = text; + const uploadedUrls: string[] = []; - let normalizedText = ""; - let cursor = 0; + if (markdownMatches.length > 0) { + let rebuilt = ""; + let cursor = 0; - for (const match of matches) { - const matchText = match[0]; - const dataUrl = match[2]; - const start = match.index ?? -1; - if (start < 0) continue; + for (const match of markdownMatches) { + const matchText = match[0]; + const dataUrl = match[2]; + const start = match.index ?? -1; + if (start < 0) continue; - normalizedText += text.slice(cursor, start); + rebuilt += normalizedText.slice(cursor, start); - try { - const uploadedUrl = await uploadDataUrlImage(dataUrl); - normalizedText += `![thumb|||${uploadedUrl}|||${imageSize}](${uploadedUrl})`; - } catch (error) { - console.error("[Chat API] Failed to upload inline data URL image:", error); - normalizedText += matchText; + try { + const uploadedUrl = await uploadDataUrlImage(dataUrl); + uploadedUrls.push(uploadedUrl); + rebuilt += `![thumb|||${uploadedUrl}|||${imageSize}](${uploadedUrl})`; + } catch (error) { + console.error("[Chat API] Failed to upload inline data URL image:", error); + rebuilt += matchText; + } + + cursor = start + matchText.length; } - cursor = start + matchText.length; + rebuilt += normalizedText.slice(cursor); + normalizedText = rebuilt; + } + + if (normalizedText.includes("data:image")) { + const rawDataUrlRegex = /data:image\/[a-z0-9+.-]+;base64,[a-z0-9+/=\s]+/gim; + const rawMatches = normalizedText.match(rawDataUrlRegex) ?? []; + + for (const rawDataUrl of rawMatches) { + try { + const uploadedUrl = await uploadDataUrlImage(rawDataUrl); + uploadedUrls.push(uploadedUrl); + normalizedText = normalizedText.replace(rawDataUrl, uploadedUrl); + } catch (error) { + console.error("[Chat API] Failed to normalize raw data URL image:", error); + } + } + } + + const hasMarkdownImage = /!\[[^\]]*\]\((https?:\/\/[^)]+)\)/i.test(normalizedText); + if (!hasMarkdownImage && uploadedUrls.length > 0) { + const uniqueUrls = [...new Set(uploadedUrls)]; + console.log( + `[Chat API] Converted ${uniqueUrls.length} data URL image(s) to hosted link(s)` + ); + return uniqueUrls + .map((url) => `![thumb|||${url}|||${imageSize}](${url})`) + .join("\n"); } - normalizedText += text.slice(cursor); return normalizedText; } diff --git a/app/canvas/[id]/page.tsx b/app/canvas/[id]/page.tsx index a3a2533..1d05b88 100644 --- a/app/canvas/[id]/page.tsx +++ b/app/canvas/[id]/page.tsx @@ -465,7 +465,7 @@ export default function CanvasEditorPage({ params }: PageProps) { return ( -
+
{/* 1. Canvas Layer (Full Screen) - 使用 key 强制在 id 变化时重新挂载 */}
diff --git a/components/canvas/CanvasChatPanel.tsx b/components/canvas/CanvasChatPanel.tsx index 5290b2a..e2ceaca 100644 --- a/components/canvas/CanvasChatPanel.tsx +++ b/components/canvas/CanvasChatPanel.tsx @@ -161,6 +161,10 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF // 🆕 标签悬停预览状态 const [tagPreview, setTagPreview] = useState(null); const previewTimeoutRef = useRef(null); + const [fullscreenPreview, setFullscreenPreview] = useState<{ + url: string; + alt?: string; + } | null>(null); // 🆕 图片参数状态 const [imageParams, setImageParams] = useState({ @@ -192,6 +196,17 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF } }, []); + useEffect(() => { + if (!fullscreenPreview) return; + const handleEsc = (event: KeyboardEvent) => { + if (event.key === "Escape") { + setFullscreenPreview(null); + } + }; + window.addEventListener("keydown", handleEsc); + return () => window.removeEventListener("keydown", handleEsc); + }, [fullscreenPreview]); + // Canvas store const addNode = useCanvasStore((state) => state.addNode); const nodes = useCanvasStore((state) => state.nodes); @@ -1419,9 +1434,14 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF window.open(url, '_blank')} + onClick={() => setFullscreenPreview({ url })} + onDoubleClick={(e) => { + e.preventDefault(); + e.stopPropagation(); + setFullscreenPreview({ url }); + }} />
))} @@ -1466,6 +1486,35 @@ export function CanvasChatPanel({ chatId, initialMessages = [], onClose, onOpenF
+ + {fullscreenPreview && createPortal( +
setFullscreenPreview(null)} + > + + + {fullscreenPreview.alt e.stopPropagation()} + /> + +
+ 点击空白处或按 Esc 关闭 +
+
, + document.body + )} {/* 底部输入区域 - 混合输入(图片标签+文字可自由混合) */}
diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 8847378..8afd10b 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -77,6 +77,21 @@ export function CanvasWorkspace() { return () => window.removeEventListener('resize', updateSize); }, []); + // macOS 触控板横向双指滑动会触发浏览器前进/后退,必须使用非 passive 监听阻止默认行为 + React.useEffect(() => { + const container = containerRef.current; + if (!container) return; + + const preventTrackpadHistorySwipe = (event: WheelEvent) => { + event.preventDefault(); + }; + + container.addEventListener("wheel", preventTrackpadHistorySwipe, { passive: false }); + return () => { + container.removeEventListener("wheel", preventTrackpadHistorySwipe); + }; + }, []); + const isFileDrag = useCallback((dataTransfer?: DataTransfer | null) => { if (!dataTransfer) return false; const types = Array.from(dataTransfer.types || []); @@ -599,7 +614,7 @@ export function CanvasWorkspace() {