"use client"; import { useEffect, useRef, useState, type MouseEvent, } from "react"; import { Handle, Position, type Node, type NodeProps } from "@xyflow/react"; import { useMutation } from "convex/react"; import { ExternalLink, ImageIcon } from "lucide-react"; import BaseNodeWrapper from "./base-node-wrapper"; import { AssetBrowserPanel, type AssetBrowserSessionState, } from "@/components/canvas/asset-browser-panel"; import { api } from "@/convex/_generated/api"; import type { Id } from "@/convex/_generated/dataModel"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { computeMediaNodeSize } from "@/lib/canvas-utils"; type AssetNodeData = { assetId?: number; assetType?: "photo" | "vector" | "icon"; title?: string; previewUrl?: string; intrinsicWidth?: number; intrinsicHeight?: number; url?: string; sourceUrl?: string; license?: "freemium" | "premium"; authorName?: string; orientation?: string; canvasId?: string; _status?: string; _statusMessage?: string; }; export type AssetNodeType = Node; export default function AssetNode({ id, data, selected, width, height }: NodeProps) { const [panelOpen, setPanelOpen] = useState(false); const [loadedPreviewUrl, setLoadedPreviewUrl] = useState(null); const [failedPreviewUrl, setFailedPreviewUrl] = useState(null); const [browserState, setBrowserState] = useState({ term: "", assetType: "photo", results: [], page: 1, totalPages: 1, }); const resizeNode = useMutation(api.nodes.resize); const hasAsset = typeof data.assetId === "number"; const previewUrl = data.url ?? data.previewUrl; const isPreviewLoading = Boolean( previewUrl && previewUrl !== loadedPreviewUrl && previewUrl !== failedPreviewUrl, ); const previewLoadError = Boolean(previewUrl && previewUrl === failedPreviewUrl); const hasAutoSizedRef = useRef(false); const rootRef = useRef(null); const headerRef = useRef(null); const previewRef = useRef(null); const imageRef = useRef(null); const footerRef = useRef(null); const lastMetricsRef = useRef(""); useEffect(() => { if (!hasAsset) return; if (hasAutoSizedRef.current) return; hasAutoSizedRef.current = true; const targetSize = computeMediaNodeSize("asset", { intrinsicWidth: data.intrinsicWidth, intrinsicHeight: data.intrinsicHeight, orientation: data.orientation, }); if (width === targetSize.width && height === targetSize.height) { return; } void resizeNode({ nodeId: id as Id<"nodes">, width: targetSize.width, height: targetSize.height, }); }, [ data.intrinsicHeight, data.intrinsicWidth, data.orientation, hasAsset, height, id, resizeNode, width, ]); const stopNodeClickPropagation = (event: MouseEvent) => { event.stopPropagation(); }; const showPreview = Boolean(hasAsset && previewUrl); useEffect(() => { if (!selected) return; const rootEl = rootRef.current; const headerEl = headerRef.current; if (!rootEl || !headerEl) return; const rootHeight = rootEl.getBoundingClientRect().height; const headerHeight = headerEl.getBoundingClientRect().height; const previewHeight = previewRef.current?.getBoundingClientRect().height ?? null; const footerHeight = footerRef.current?.getBoundingClientRect().height ?? null; const imageEl = imageRef.current; const rootStyles = window.getComputedStyle(rootEl); const imageStyles = imageEl ? window.getComputedStyle(imageEl) : null; const rows = rootStyles.gridTemplateRows; const imageRect = imageEl?.getBoundingClientRect(); const previewRect = previewRef.current?.getBoundingClientRect(); const naturalRatio = imageEl && imageEl.naturalWidth > 0 && imageEl.naturalHeight > 0 ? imageEl.naturalWidth / imageEl.naturalHeight : null; const previewRatio = previewRect && previewRect.width > 0 && previewRect.height > 0 ? previewRect.width / previewRect.height : null; let expectedContainWidth: number | null = null; let expectedContainHeight: number | null = null; if (previewRect && naturalRatio) { const fitByWidthHeight = previewRect.width / naturalRatio; if (fitByWidthHeight <= previewRect.height) { expectedContainWidth = previewRect.width; expectedContainHeight = fitByWidthHeight; } else { expectedContainHeight = previewRect.height; expectedContainWidth = previewRect.height * naturalRatio; } } const signature = `${width}|${height}|${Math.round(rootHeight)}|${Math.round(headerHeight)}|${Math.round(previewHeight ?? -1)}|${Math.round(footerHeight ?? -1)}|${Math.round(imageRect?.height ?? -1)}|${rows}|${showPreview}`; if (lastMetricsRef.current === signature) { return; } lastMetricsRef.current = signature; // #region agent log fetch('http://127.0.0.1:7733/ingest/db1ec129-24cb-483b-98e2-3e7beef6d9cd',{method:'POST',headers:{'Content-Type':'application/json','X-Debug-Session-Id':'d48a18'},body:JSON.stringify({sessionId:'d48a18',runId:'run4',hypothesisId:'H13-H14',location:'asset-node.tsx:metricsEffect',message:'asset contain-fit diagnostics',data:{nodeId:id,width,height,rootHeight,previewWidth:previewRect?.width ?? null,previewHeight,previewRatio,naturalRatio,headerHeight,footerHeight,imageRenderWidth:imageRect?.width ?? null,imageRenderHeight:imageRect?.height ?? null,expectedContainWidth,expectedContainHeight,imageNaturalWidth:imageEl?.naturalWidth ?? null,imageNaturalHeight:imageEl?.naturalHeight ?? null,imageObjectFit:imageStyles?.objectFit ?? null,imageObjectPosition:imageStyles?.objectPosition ?? null,rows,showPreview},timestamp:Date.now()})}).catch(()=>{}); // #endregion }, [height, id, selected, showPreview, width]); return (
Asset
{showPreview ? ( <>
{isPreviewLoading ? (
Loading preview...
) : null} {previewLoadError ? (
Preview unavailable
) : null} {/* eslint-disable-next-line @next/next/no-img-element */} {data.title { setLoadedPreviewUrl(previewUrl ?? null); setFailedPreviewUrl((current) => current === (previewUrl ?? null) ? null : current, ); }} onError={() => { setFailedPreviewUrl(previewUrl ?? null); }} /> {data.assetType ?? "asset"} {data.license ? ( {data.license} ) : null}

{data.title ?? "Untitled"}

by {data.authorName ?? "Freepik"} {data.sourceUrl ? ( freepik.com ) : null}
) : (

No asset selected

Browse millions of Freepik resources

)}
{panelOpen && data.canvasId ? ( setPanelOpen(false)} /> ) : null}
); }