fix: improve canvas drag drop and placeholder timing
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

Harden desktop drag-and-drop on the canvas and ensure auto image placeholders wait for visible assistant text.
This commit is contained in:
Your Name 2026-02-03 19:36:12 +08:00
parent 0cc791a71e
commit 2e6f421b6d
2 changed files with 116 additions and 7 deletions

View File

@ -41,6 +41,7 @@ export function CanvasWorkspace() {
const [isPanning, setIsPanning] = useState(false);
const [panStart, setPanStart] = useState<Position>({ 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<HTMLDivElement>) => {
if (!isFileDrag(e.dataTransfer)) return;
e.preventDefault();
dragCounterRef.current += 1;
}, [isFileDrag]);
const handleDragLeave = useCallback((e: React.DragEvent<HTMLDivElement>) => {
if (!isFileDrag(e.dataTransfer)) return;
e.preventDefault();
dragCounterRef.current = Math.max(0, dragCounterRef.current - 1);
}, [isFileDrag]);
const handleDragOver = useCallback((e: React.DragEvent<HTMLDivElement>) => {
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<HTMLDivElement>) => {
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) => {

View File

@ -133,6 +133,17 @@ function PureMessages({
// 控制等待动画延迟消失(Gemini 图片模型延迟 4 秒,其他模型 6 秒)
const [showThinkingWithDelay, setShowThinkingWithDelay] = useState(false);
const [autoImagePlaceholderReady, setAutoImagePlaceholderReady] = useState(false);
const autoImagePlaceholderTimerRef = useRef<ReturnType<typeof setTimeout> | 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("<!-- GENERATING_IMAGE -->")) return false;
const cleanedText = part.text.replace(/\n*<!-- GENERATING_IMAGE -->\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 <ImageGenerationLoading imageCount={imageCount} maxImages={6} providerType="alibaba-image" modelName="智能处理" />;
}
// 否则显示思考动画(在等待 AI 分析或延迟期间)