feat: enhance dashboard and canvas components with improved state management and resizing logic
- Added client mount state to the dashboard to prevent premature interactions before the component is fully loaded. - Updated button disabling logic to ensure it reflects the component's readiness and user session state. - Introduced zIndex handling in canvas placement context for better node layering. - Enhanced asset and image nodes with improved resizing logic to maintain aspect ratios during adjustments. - Refactored node components to streamline rendering and improve performance during dynamic updates.
This commit is contained in:
@@ -18,7 +18,7 @@ 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";
|
||||
import { resolveMediaAspectRatio } from "@/lib/canvas-utils";
|
||||
|
||||
type AssetNodeData = {
|
||||
assetId?: number;
|
||||
@@ -60,28 +60,40 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
const previewLoadError = Boolean(previewUrl && previewUrl === failedPreviewUrl);
|
||||
|
||||
const hasAutoSizedRef = useRef(false);
|
||||
const rootRef = useRef<HTMLDivElement>(null);
|
||||
const headerRef = useRef<HTMLDivElement>(null);
|
||||
const previewRef = useRef<HTMLDivElement>(null);
|
||||
const imageRef = useRef<HTMLImageElement>(null);
|
||||
const footerRef = useRef<HTMLDivElement>(null);
|
||||
const lastMetricsRef = useRef<string>("");
|
||||
|
||||
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) {
|
||||
const targetAspectRatio = resolveMediaAspectRatio(
|
||||
data.intrinsicWidth,
|
||||
data.intrinsicHeight,
|
||||
data.orientation,
|
||||
);
|
||||
const minimumNodeHeight = 208;
|
||||
const baseNodeWidth = 260;
|
||||
const targetWidth = Math.max(baseNodeWidth, Math.round(minimumNodeHeight * targetAspectRatio));
|
||||
const targetHeight = Math.round(targetWidth / targetAspectRatio);
|
||||
const targetSize = {
|
||||
width: targetWidth,
|
||||
height: targetHeight,
|
||||
};
|
||||
const currentWidth = typeof width === "number" ? width : 0;
|
||||
const currentHeight = typeof height === "number" ? height : 0;
|
||||
const hasMeasuredSize = currentWidth > 0 && currentHeight > 0;
|
||||
if (!hasMeasuredSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
const isAtTargetSize = currentWidth === targetSize.width && currentHeight === targetSize.height;
|
||||
const isAtDefaultSeedSize = currentWidth === 260 && currentHeight === 240;
|
||||
const shouldRunInitialAutoSize = isAtDefaultSeedSize && !isAtTargetSize;
|
||||
|
||||
if (!shouldRunInitialAutoSize) {
|
||||
hasAutoSizedRef.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
hasAutoSizedRef.current = true;
|
||||
void resizeNode({
|
||||
nodeId: id as Id<"nodes">,
|
||||
width: targetSize.width,
|
||||
@@ -104,61 +116,12 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
|
||||
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 (
|
||||
<BaseNodeWrapper
|
||||
nodeType="asset"
|
||||
selected={selected}
|
||||
status={data._status}
|
||||
statusMessage={data._statusMessage}
|
||||
className="overflow-hidden"
|
||||
>
|
||||
<Handle
|
||||
type="target"
|
||||
@@ -167,14 +130,13 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
/>
|
||||
|
||||
<div
|
||||
ref={rootRef}
|
||||
className={`grid h-full min-h-0 w-full ${
|
||||
showPreview
|
||||
? "grid-rows-[auto_minmax(0,1fr)_auto]"
|
||||
: "grid-rows-[auto_minmax(0,1fr)]"
|
||||
}`}
|
||||
>
|
||||
<div ref={headerRef} className="flex items-center justify-between border-b px-3 py-2">
|
||||
<div className="flex items-center justify-between border-b px-3 py-2">
|
||||
<span className="text-xs font-medium tracking-wide text-muted-foreground uppercase">
|
||||
Asset
|
||||
</span>
|
||||
@@ -191,7 +153,7 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
|
||||
{showPreview ? (
|
||||
<>
|
||||
<div ref={previewRef} className="relative min-h-0 overflow-hidden bg-muted/30">
|
||||
<div className="relative min-h-0 overflow-hidden bg-muted/30">
|
||||
{isPreviewLoading ? (
|
||||
<div className="absolute inset-0 z-10 flex animate-pulse items-center justify-center bg-muted/60 text-[11px] text-muted-foreground">
|
||||
Loading preview...
|
||||
@@ -204,10 +166,9 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
) : null}
|
||||
{/* eslint-disable-next-line @next/next/no-img-element */}
|
||||
<img
|
||||
ref={imageRef}
|
||||
src={previewUrl}
|
||||
alt={data.title ?? "Asset preview"}
|
||||
className={`h-full w-full object-cover object-center transition-opacity ${
|
||||
className={`h-full w-full object-cover object-right transition-opacity ${
|
||||
isPreviewLoading ? "opacity-0" : "opacity-100"
|
||||
}`}
|
||||
draggable={false}
|
||||
@@ -234,7 +195,7 @@ export default function AssetNode({ id, data, selected, width, height }: NodePro
|
||||
) : null}
|
||||
</div>
|
||||
|
||||
<div ref={footerRef} className="flex flex-col gap-1 px-3 py-2">
|
||||
<div className="flex flex-col gap-1 px-3 py-2">
|
||||
<p className="truncate text-xs font-medium" title={data.title ?? "Untitled"}>
|
||||
{data.title ?? "Untitled"}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user