diff --git a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx
index 4b15fdd..5915ab4 100644
--- a/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx
+++ b/understand-anything-plugin/packages/dashboard/src/components/ExportMenu.tsx
@@ -41,6 +41,63 @@ export default function ExportMenu() {
return () => document.removeEventListener("mousedown", handleClickOutside);
}, [exportMenuOpen, toggleExportMenu]);
+ const buildCleanSvg = () => {
+ if (!reactFlowInstance) return null;
+
+ const nodes = reactFlowInstance.getNodes();
+ const edges = reactFlowInstance.getEdges();
+ if (nodes.length === 0) return null;
+
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
+ nodes.forEach((node) => {
+ const x = node.position.x;
+ const y = node.position.y;
+ const width = (node.width ?? 200);
+ const height = (node.height ?? 80);
+ minX = Math.min(minX, x);
+ minY = Math.min(minY, y);
+ maxX = Math.max(maxX, x + width);
+ maxY = Math.max(maxY, y + height);
+ });
+
+ const padding = 40;
+ const width = maxX - minX + padding * 2;
+ const height = maxY - minY + padding * 2;
+ const offsetX = -minX + padding;
+ const offsetY = -minY + padding;
+
+ let svgContent = ``;
+ return { svgContent, width, height };
+ };
+
const exportPNG = async () => {
if (!reactFlowInstance) {
alert("Graph not ready for export");
@@ -48,63 +105,20 @@ export default function ExportMenu() {
}
try {
- // Get the viewport element
- const viewport = document.querySelector(".react-flow__viewport") as HTMLElement;
- if (!viewport) {
- throw new Error("Viewport not found");
- }
-
- // Get the bounding box of all nodes
- const nodes = reactFlowInstance.getNodes();
- if (nodes.length === 0) {
+ const result = buildCleanSvg();
+ if (!result) {
alert("No nodes to export");
return;
}
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- nodes.forEach((node) => {
- const x = node.position.x;
- const y = node.position.y;
- const width = (node.width ?? 200);
- const height = (node.height ?? 80);
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x + width);
- maxY = Math.max(maxY, y + height);
- });
-
- const padding = 40;
- const width = maxX - minX + padding * 2;
- const height = maxY - minY + padding * 2;
-
- // Clone the viewport
- const clone = viewport.cloneNode(true) as HTMLElement;
- clone.style.transform = `translate(${-minX + padding}px, ${-minY + padding}px)`;
-
- // Create an SVG with foreignObject
- const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
- svg.setAttribute("width", String(width * 2));
- svg.setAttribute("height", String(height * 2));
- svg.setAttribute("viewBox", `0 0 ${width} ${height}`);
- svg.style.backgroundColor = "#0a0a0a";
-
- const foreignObject = document.createElementNS("http://www.w3.org/2000/svg", "foreignObject");
- foreignObject.setAttribute("width", "100%");
- foreignObject.setAttribute("height", "100%");
- foreignObject.appendChild(clone);
- svg.appendChild(foreignObject);
-
- // Serialize SVG to string
- const svgString = new XMLSerializer().serializeToString(svg);
- const svgBlob = new Blob([svgString], { type: "image/svg+xml;charset=utf-8" });
+ const { svgContent, width, height } = result;
+ const svgBlob = new Blob([svgContent], { type: "image/svg+xml;charset=utf-8" });
const url = URL.createObjectURL(svgBlob);
- // Create an image and draw to canvas
const img = new Image();
img.onerror = () => {
URL.revokeObjectURL(url);
- console.error("PNG export failed: Image failed to load from SVG blob");
- alert("Failed to export PNG: could not render graph as image. Try SVG export instead.");
+ alert("Failed to export PNG: could not render graph as image.");
};
img.onload = () => {
const canvas = document.createElement("canvas");
@@ -116,7 +130,7 @@ export default function ExportMenu() {
alert("Failed to create canvas context");
return;
}
- ctx.drawImage(img, 0, 0);
+ ctx.drawImage(img, 0, 0, width * 2, height * 2);
URL.revokeObjectURL(url);
const filename = `${graph?.project.name ?? "knowledge-graph"}-export.png`;
@@ -125,8 +139,7 @@ export default function ExportMenu() {
downloadBlob(blob, filename);
toggleExportMenu();
} else {
- console.error("PNG export failed: canvas.toBlob returned null (canvas may be tainted)");
- alert("Failed to export PNG: image encoding failed. Try SVG export instead.");
+ alert("Failed to export PNG: image encoding failed.");
}
}, "image/png");
};
@@ -144,67 +157,13 @@ export default function ExportMenu() {
}
try {
- const nodes = reactFlowInstance.getNodes();
- const edges = reactFlowInstance.getEdges();
-
- if (nodes.length === 0) {
+ const result = buildCleanSvg();
+ if (!result) {
alert("No nodes to export");
return;
}
- // Calculate bounding box
- let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
- nodes.forEach((node) => {
- const x = node.position.x;
- const y = node.position.y;
- const width = (node.width ?? 200);
- const height = (node.height ?? 80);
- minX = Math.min(minX, x);
- minY = Math.min(minY, y);
- maxX = Math.max(maxX, x + width);
- maxY = Math.max(maxY, y + height);
- });
-
- const padding = 40;
- const width = maxX - minX + padding * 2;
- const height = maxY - minY + padding * 2;
- const offsetX = -minX + padding;
- const offsetY = -minY + padding;
-
- // Build SVG manually
- let svgContent = ``;
-
- const blob = new Blob([svgContent], { type: "image/svg+xml;charset=utf-8" });
+ const blob = new Blob([result.svgContent], { type: "image/svg+xml;charset=utf-8" });
const filename = `${graph?.project.name ?? "knowledge-graph"}-export.svg`;
downloadBlob(blob, filename);
toggleExportMenu();