From 2e6f421b6d192060d071411d2198f9b23ba6eabe Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 3 Feb 2026 19:36:12 +0800 Subject: [PATCH] fix: improve canvas drag drop and placeholder timing Harden desktop drag-and-drop on the canvas and ensure auto image placeholders wait for visible assistant text. --- components/canvas/CanvasWorkspace.tsx | 63 ++++++++++++++++++++++++--- components/messages.tsx | 60 ++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/components/canvas/CanvasWorkspace.tsx b/components/canvas/CanvasWorkspace.tsx index 82e56293b..c5be68cd6 100644 --- a/components/canvas/CanvasWorkspace.tsx +++ b/components/canvas/CanvasWorkspace.tsx @@ -41,6 +41,7 @@ export function CanvasWorkspace() { const [isPanning, setIsPanning] = useState(false); const [panStart, setPanStart] = useState({ x: 0, y: 0 }); const [containerSize, setContainerSize] = useState({ width: 1920, height: 1080 }); + const dragCounterRef = useRef(0); // Store state const nodes = useCanvasStore(canvasSelectors.nodes); @@ -64,6 +65,43 @@ export function CanvasWorkspace() { window.addEventListener('resize', updateSize); return () => window.removeEventListener('resize', updateSize); }, []); + + const isFileDrag = useCallback((dataTransfer?: DataTransfer | null) => { + if (!dataTransfer) return false; + const types = Array.from(dataTransfer.types || []); + if ( + types.includes("Files") || + types.includes("application/x-moz-file") || + types.includes("public.file-url") + ) { + return true; + } + const items = dataTransfer.items; + if (items && Array.from(items).some((item) => item.kind === "file")) { + return true; + } + return false; + }, []); + + // 防止浏览器默认打开文件 + React.useEffect(() => { + const handleWindowDragOver = (e: DragEvent) => { + if (isFileDrag(e.dataTransfer)) { + e.preventDefault(); + } + }; + const handleWindowDrop = (e: DragEvent) => { + if (isFileDrag(e.dataTransfer)) { + e.preventDefault(); + } + }; + window.addEventListener("dragover", handleWindowDragOver); + window.addEventListener("drop", handleWindowDrop); + return () => { + window.removeEventListener("dragover", handleWindowDragOver); + window.removeEventListener("drop", handleWindowDrop); + }; + }, [isFileDrag]); // 性能优化:只渲染视口内的节点(虚拟化) const visibleNodes = useMemo(() => { @@ -212,16 +250,29 @@ export function CanvasWorkspace() { }); }, []); + const handleDragEnter = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + dragCounterRef.current += 1; + }, [isFileDrag]); + + const handleDragLeave = useCallback((e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + dragCounterRef.current = Math.max(0, dragCounterRef.current - 1); + }, [isFileDrag]); + const handleDragOver = useCallback((e: React.DragEvent) => { - if (e.dataTransfer?.types?.includes?.("Files")) { - e.preventDefault(); - e.dataTransfer.dropEffect = "copy"; - } - }, []); + if (!isFileDrag(e.dataTransfer)) return; + e.preventDefault(); + e.dataTransfer.dropEffect = "copy"; + }, [isFileDrag]); const handleDrop = useCallback(async (e: React.DragEvent) => { + if (!isFileDrag(e.dataTransfer)) return; e.preventDefault(); e.stopPropagation(); + dragCounterRef.current = 0; const files = Array.from(e.dataTransfer?.files || []).filter((file) => file.type.startsWith("image/")); if (files.length === 0) return; @@ -282,7 +333,7 @@ export function CanvasWorkspace() { toast.error("图片上传失败"); } } - }, [addNode, getImageDimensionsFromFile, updateNode, viewport.x, viewport.y, viewport.zoom]); + }, [addNode, getImageDimensionsFromFile, isFileDrag, updateNode, viewport.x, viewport.y, viewport.zoom]); // 渲染节点 const renderNode = (node: CanvasNode) => { diff --git a/components/messages.tsx b/components/messages.tsx index 6ecb5c76f..1faff2fd6 100644 --- a/components/messages.tsx +++ b/components/messages.tsx @@ -133,6 +133,17 @@ function PureMessages({ // 控制等待动画延迟消失(Gemini 图片模型延迟 4 秒,其他模型 6 秒) const [showThinkingWithDelay, setShowThinkingWithDelay] = useState(false); + const [autoImagePlaceholderReady, setAutoImagePlaceholderReady] = useState(false); + const autoImagePlaceholderTimerRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (autoImagePlaceholderTimerRef.current) { + clearTimeout(autoImagePlaceholderTimerRef.current); + autoImagePlaceholderTimerRef.current = null; + } + }; + }, []); useEffect(() => { const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null; @@ -299,6 +310,53 @@ function PureMessages({ } }, [status, messages, isGeminiModel, isQianwenVideoModel, isVolcanoVisionModel, isVideoModel, isAlibabaImageModel, selectedModelId, isGeneratingImageFlag]); + // 🆕 智能自动模式:确保文字可见后再显示生图占位图 + useEffect(() => { + const lastUserMessage = [...messages].reverse().find(m => m.role === "user"); + const isAutoModeWithImage = selectedModelId === "auto" && + lastUserMessage?.parts?.some((p: any) => p.type === "file" || p.type === "image"); + + const hasAssistantTextReplyWithContent = messages.some(msg => + msg.role === "assistant" && msg.parts?.some((part: any) => { + if (part.type !== "text" || !part.text) return false; + if (!part.text.includes("")) return false; + const cleanedText = part.text.replace(/\n*\n*/g, "").trim(); + return cleanedText.length > 0; + }) + ); + + let hasImageContent = false; + if (isAutoModeWithImage) { + const lastUserIndex = messages.findLastIndex(m => m.role === "user"); + const assistantMessagesAfterUser = messages.slice(lastUserIndex + 1).filter(m => m.role === "assistant"); + hasImageContent = assistantMessagesAfterUser.some(msg => + msg.parts?.some((part: any) => part.type === "text" && part.text && ( + part.text.includes("![thumb|") || + part.text.includes("![thumb|||") || + part.text.includes("![Generated Image") + )) + ); + } + + if (!isAutoModeWithImage || !isGeneratingImageFlag || !hasAssistantTextReplyWithContent || hasImageContent) { + if (autoImagePlaceholderTimerRef.current) { + clearTimeout(autoImagePlaceholderTimerRef.current); + autoImagePlaceholderTimerRef.current = null; + } + if (autoImagePlaceholderReady) { + setAutoImagePlaceholderReady(false); + } + return; + } + + if (!autoImagePlaceholderReady && !autoImagePlaceholderTimerRef.current) { + autoImagePlaceholderTimerRef.current = setTimeout(() => { + setAutoImagePlaceholderReady(true); + autoImagePlaceholderTimerRef.current = null; + }, 800); + } + }, [messages, selectedModelId, isGeneratingImageFlag, autoImagePlaceholderReady]); + // 🆕 监听新消息添加,当用户发送带图片的消息或添加加载消息时,强制滚动到底部 const prevMessagesLengthRef = useRef(messages.length); useEffect(() => { @@ -490,7 +548,7 @@ function PureMessages({ if (isAutoMode && userMessageHasImage) { // 🆕 只用 isGeneratingImageFlag 来控制是否显示生图占位图 // isGeneratingImageFlag 由 chat.tsx 控制:延迟 3 秒后设置为 true,收到图片后设置为 false - if (isGeneratingImageFlag && hasAssistantTextReplyWithContent) { + if (autoImagePlaceholderReady && isGeneratingImageFlag && hasAssistantTextReplyWithContent) { return ; } // 否则显示思考动画(在等待 AI 分析或延迟期间)