Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature/export button throttle debounce #102

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
163 changes: 92 additions & 71 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,87 +27,108 @@ export default function Home() {
return () => {
window.removeEventListener("keydown", listener);
};
});
}, []);

return (
<>
<div className={`w-screen h-screen`}>
<Tldraw persistenceKey="tldraw">
<ExportButton setHtml={setHtml} />
</Tldraw>
</div>
{html &&
ReactDOM.createPortal(
<div
className="fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center"
style={{ zIndex: 2000, backgroundColor: "rgba(0,0,0,0.5)" }}
onClick={() => setHtml(null)}
>
<PreviewModal html={html} setHtml={setHtml} />
</div>,
document.body
)}
</>
<>
<div className="w-screen h-screen">
<Tldraw persistenceKey="tldraw">
<ExportButton setHtml={setHtml} />
</Tldraw>
</div>
{html &&
ReactDOM.createPortal(
<div
className="fixed top-0 left-0 right-0 bottom-0 flex justify-center items-center"
style={{ zIndex: 2000, backgroundColor: "rgba(0,0,0,0.5)" }}
onClick={() => setHtml(null)}
>
<PreviewModal html={html} setHtml={setHtml} />
</div>,
document.body
)}
</>
);
}

function ExportButton({ setHtml }: { setHtml: (html: string) => void }) {
const editor = useEditor();
const [loading, setLoading] = useState(false);
// A tailwind styled button that is pinned to the bottom right of the screen
return (
<button
onClick={async (e) => {
setLoading(true);
try {
e.preventDefault();
const svg = await editor.getSvg(
Array.from(editor.currentPageShapeIds)
);
if (!svg) {
return;
}
const png = await getSvgAsImage(svg, {
type: "png",
quality: 1,
scale: 1,
});
const dataUrl = await blobToBase64(png!);
const resp = await fetch("/api/toHtml", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ image: dataUrl }),
});

const json = await resp.json();

if (json.error) {
alert("Error from open ai: " + JSON.stringify(json.error));

const throttle = (func: (...args: any[]) => void, limit: number) => {
let lastFunc: NodeJS.Timeout | null = null;
let lastRan: number | null = null;

return function (this: any, ...args: any[]) {
const context = this;

if (lastRan === null || Date.now() - lastRan >= limit) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc!);
lastFunc = setTimeout(() => {
func.apply(context, args);
}, limit - (Date.now() - lastRan));
}
};
};

const handleExport = async () => {
setLoading(true);
try {
const svg = await editor.getSvg(Array.from(editor.currentPageShapeIds));
if (!svg) return;

const png = await getSvgAsImage(svg, { type: "png", quality: 1, scale: 1 });
const dataUrl = await blobToBase64(png!);

const sendRequest = async () => {
const resp = await fetch("/api/toHtml", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ image: dataUrl }),
});
const json = await resp.json();
if (json.error) {
if (confirm("Error from open ai: " + JSON.stringify(json.error) + ". Retry?")) {
await sendRequest(); // Retry the request
} else {
return;
}

const message = json.choices[0].message.content;
const start = message.indexOf("<!DOCTYPE html>");
const end = message.indexOf("</html>");
const html = message.slice(start, end + "</html>".length);
setHtml(html);
} finally {
setLoading(false);
}
}}
className="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded ="
style={{ zIndex: 1000 }}
disabled={loading}
>
{loading ? (
<div className="flex justify-center items-center ">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
</div>
) : (
"Make Real"
)}
</button>
const message = json.choices[0].message.content;
const start = message.indexOf("<!DOCTYPE html>");
const end = message.indexOf("</html>");
const html = message.slice(start, end + "</html>".length);
setHtml(html);
};

await sendRequest();
} finally {
setLoading(false);
}
};

const throttledHandleExport = throttle(handleExport, 2000); // 2000ms 节流

return (
<button
onClick={(e) => {
e.preventDefault();
throttledHandleExport();
}}
className="fixed bottom-4 right-4 bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded"
style={{ zIndex: 1000 }}
disabled={loading}
>
{loading ? (
<div className="flex justify-center items-center">
<div className="animate-spin rounded-full h-4 w-4 border-b-2 border-white"></div>
</div>
) : (
"Make Real"
)}
</button>
);
}