(null);
@@ -388,6 +389,108 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
const handleCancelPicking = useCallback(() => {
setPickerMode(false);
}, [setPickerMode]);
+ const displayTitle = getImageNodeTitle(data, node.id);
+
+ const animateAddToChat = useCallback(async () => {
+ if (!containerRef.current || !data.src || typeof window === "undefined") {
+ return false;
+ }
+
+ const target = document.querySelector('[data-canvas-chat-input-anchor="true"]') as HTMLElement | null;
+ if (!target) return false;
+
+ const sourceImage =
+ (containerRef.current.querySelector("img") as HTMLImageElement | null) || containerRef.current;
+ const sourceRect = sourceImage.getBoundingClientRect();
+ const targetRect = target.getBoundingClientRect();
+
+ if (sourceRect.width <= 0 || sourceRect.height <= 0 || targetRect.width <= 0 || targetRect.height <= 0) {
+ return false;
+ }
+
+ const clone = document.createElement("img");
+ clone.src = data.src;
+ clone.alt = "";
+ clone.style.position = "fixed";
+ clone.style.left = `${sourceRect.left}px`;
+ clone.style.top = `${sourceRect.top}px`;
+ clone.style.width = `${sourceRect.width}px`;
+ clone.style.height = `${sourceRect.height}px`;
+ clone.style.objectFit = "cover";
+ clone.style.borderRadius = "14px";
+ clone.style.pointerEvents = "none";
+ clone.style.zIndex = "99999";
+ clone.style.boxShadow = "0 14px 30px rgba(15, 23, 42, 0.28)";
+ clone.style.transformOrigin = "center center";
+ clone.style.transition =
+ "transform 560ms cubic-bezier(0.22, 1, 0.36, 1), opacity 560ms ease, filter 560ms ease";
+ clone.style.willChange = "transform, opacity, filter";
+ document.body.appendChild(clone);
+
+ const sourceCenterX = sourceRect.left + sourceRect.width / 2;
+ const sourceCenterY = sourceRect.top + sourceRect.height / 2;
+ const targetCenterX = targetRect.left + targetRect.width / 2;
+ const targetCenterY = targetRect.top + targetRect.height / 2;
+ const scale = Math.max(0.12, Math.min(0.24, Math.min(targetRect.width / sourceRect.width, targetRect.height / sourceRect.height)));
+
+ target.animate(
+ [
+ { boxShadow: "0 0 0 0 rgba(59,130,246,0)", transform: "scale(1)" },
+ { boxShadow: "0 0 0 6px rgba(59,130,246,0.14)", transform: "scale(1.015)" },
+ { boxShadow: "0 0 0 0 rgba(59,130,246,0)", transform: "scale(1)" },
+ ],
+ { duration: 520, easing: "cubic-bezier(0.22, 1, 0.36, 1)" }
+ );
+
+ await new Promise((resolve) => {
+ let done = false;
+ const finish = () => {
+ if (done) return;
+ done = true;
+ clone.remove();
+ resolve();
+ };
+
+ const timeout = window.setTimeout(finish, 700);
+ clone.addEventListener(
+ "transitionend",
+ () => {
+ window.clearTimeout(timeout);
+ finish();
+ },
+ { once: true }
+ );
+
+ requestAnimationFrame(() => {
+ requestAnimationFrame(() => {
+ const dx = targetCenterX - sourceCenterX;
+ const dy = targetCenterY - sourceCenterY;
+ clone.style.transform = `translate(${dx}px, ${dy}px) scale(${scale})`;
+ clone.style.opacity = "0.14";
+ clone.style.filter = "blur(1.2px)";
+ });
+ });
+ });
+
+ return true;
+ }, [data.src]);
+
+ const handleAddToChat = useCallback(async () => {
+ if (!data.src || isProcessing || isAddingToChat) return;
+ setIsAddingToChat(true);
+
+ try {
+ await animateAddToChat();
+ addPendingChatAttachment({
+ url: data.src,
+ name: displayTitle,
+ contentType: "image/jpeg",
+ });
+ toast.success("已添加到对话框");
+ } finally {
+ setIsAddingToChat(false);
+ }
+ }, [addPendingChatAttachment, animateAddToChat, data.src, displayTitle, isAddingToChat, isProcessing]);
// 获取工具栏位置 - 在渲染时直接计算
const getToolbarPosition = () => {
@@ -431,8 +534,6 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
return { top, left };
};
- const displayTitle = getImageNodeTitle(data, node.id);
-
const handleSaveTitle = useCallback(() => {
const nextTitle = normalizeImageTitle(draftTitle) || displayTitle;
updateNode(node.id, {
@@ -1564,18 +1665,15 @@ export function ImageNode({ node, isSelected }: ImageNodeProps) {
添加到对话