From 7951282376ed9805ccd263d69a55b791cbd76749 Mon Sep 17 00:00:00 2001 From: Kai Chappell Date: Wed, 21 May 2025 21:03:28 +0100 Subject: [PATCH] pyodide hook for python execution --- frontend/src/hooks/use-pyodide.ts | 123 ++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 frontend/src/hooks/use-pyodide.ts diff --git a/frontend/src/hooks/use-pyodide.ts b/frontend/src/hooks/use-pyodide.ts new file mode 100644 index 0000000..c566280 --- /dev/null +++ b/frontend/src/hooks/use-pyodide.ts @@ -0,0 +1,123 @@ +"use client"; + +import { useState, useEffect, useCallback, useRef } from "react"; + +declare global { + interface Window { + loadPyodide: (config: { indexURL: string }) => Promise; + } +} + +interface PyodideInterface { + runPythonAsync: (code: string) => Promise; + globals: { + get: (name: string) => unknown; + }; +} + +interface PyodideState { + pyodide: PyodideInterface | null; + loading: boolean; + error: string | null; +} + +interface RunResult { + output: unknown; + error: string | null; +} + +const PYODIDE_CDN = "https://cdn.jsdelivr.net/pyodide/v0.25.0/full/"; + +export function usePyodide() { + const [state, setState] = useState({ + pyodide: null, + loading: true, + error: null, + }); + const loadingRef = useRef(false); + + useEffect(() => { + if (loadingRef.current || state.pyodide) return; + loadingRef.current = true; + + const loadPyodideScript = async () => { + // Check if script is already loaded + if (window.loadPyodide) { + try { + const pyodide = await window.loadPyodide({ indexURL: PYODIDE_CDN }); + setState({ pyodide, loading: false, error: null }); + } catch (err) { + setState({ + pyodide: null, + loading: false, + error: err instanceof Error ? err.message : "Failed to load Pyodide", + }); + } + return; + } + + // Load the script + const script = document.createElement("script"); + script.src = `${PYODIDE_CDN}pyodide.js`; + script.async = true; + + script.onload = async () => { + try { + const pyodide = await window.loadPyodide({ indexURL: PYODIDE_CDN }); + setState({ pyodide, loading: false, error: null }); + } catch (err) { + setState({ + pyodide: null, + loading: false, + error: err instanceof Error ? err.message : "Failed to load Pyodide", + }); + } + }; + + script.onerror = () => { + setState({ + pyodide: null, + loading: false, + error: "Failed to load Pyodide script", + }); + }; + + document.head.appendChild(script); + }; + + loadPyodideScript(); + }, [state.pyodide]); + + const runPython = useCallback( + async (code: string, timeout = 5000): Promise => { + if (!state.pyodide) { + return { output: null, error: "Pyodide not loaded" }; + } + + return new Promise((resolve) => { + const timeoutId = setTimeout(() => { + resolve({ output: null, error: "Execution timed out" }); + }, timeout); + + state.pyodide! + .runPythonAsync(code) + .then((result) => { + clearTimeout(timeoutId); + resolve({ output: result, error: null }); + }) + .catch((err: Error) => { + clearTimeout(timeoutId); + resolve({ output: null, error: err.message }); + }); + }); + }, + [state.pyodide] + ); + + return { + pyodide: state.pyodide, + loading: state.loading, + error: state.error, + runPython, + }; +}