"use client" import { useCallback, useEffect, useState } from "react" import { Loader2, Play, RotateCw, Terminal } from "lucide-react" const PYODIDE_SCRIPT_URL = "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/pyodide.js" const PYODIDE_INDEX_URL = "https://cdn.jsdelivr.net/pyodide/v0.26.2/full/" type Pyodide = { runPythonAsync: (code: string) => Promise setStdout: (options: { batched: (text: string) => void }) => void setStderr: (options: { batched: (text: string) => void }) => void } declare global { interface Window { loadPyodide?: (options?: { indexURL?: string }) => Promise } } type PythonRunnerProps = { code: string autoRun?: boolean } type RunStatus = "idle" | "loading" | "running" | "done" | "error" let pyodideInstance: Pyodide | null = null let pyodidePromise: Promise | null = null let runQueue: Promise = Promise.resolve() export function PythonRunner({ code, autoRun = true }: PythonRunnerProps) { const [status, setStatus] = useState("idle") const [stdout, setStdoutText] = useState("") const [stderr, setStderrText] = useState("") const [executionTimeMs, setExecutionTimeMs] = useState(null) const runCode = useCallback(() => { let cancelled = false setStatus(pyodideInstance ? "running" : "loading") setStdoutText("") setStderrText("") setExecutionTimeMs(null) const queuedRun = runQueue.catch(() => undefined).then(async () => { const start = performance.now() let nextStdout = "" let nextStderr = "" try { const pyodide = await loadPyodideSingleton() if (!cancelled) setStatus("running") pyodide.setStdout({ batched: (text) => { nextStdout += text + "\n" if (!cancelled) setStdoutText(nextStdout) }, }) pyodide.setStderr({ batched: (text) => { nextStderr += text + "\n" if (!cancelled) setStderrText(nextStderr) }, }) const result = await pyodide.runPythonAsync(code) if (result !== undefined && result !== null) { nextStdout += String(result) + "\n" } if (!cancelled) { setStdoutText(nextStdout) setStderrText(nextStderr) setExecutionTimeMs(performance.now() - start) setStatus("done") } } catch (error) { nextStderr += error instanceof Error ? error.message : "Không thể chạy mã Python." if (!cancelled) { setStdoutText(nextStdout) setStderrText(nextStderr) setExecutionTimeMs(performance.now() - start) setStatus("error") } } }) runQueue = queuedRun return () => { cancelled = true } }, [code]) useEffect(() => { if (!autoRun) return return runCode() }, [autoRun, runCode]) const isLoading = status === "loading" const isRunning = status === "running" const isBusy = isLoading || isRunning return (
🐍 Python
{isLoading && (
Đang khởi tạo Python...
)} {isRunning && (
Đang chạy mã Python...
)}
          {stdout || (isBusy ? "" : "(không có stdout)")}
        
{stderr && (
            {stderr}
          
)} {executionTimeMs !== null && (
Thời gian chạy: {executionTimeMs.toFixed(0)} ms
)}
) } function loadPyodideSingleton(): Promise { if (pyodideInstance) return Promise.resolve(pyodideInstance) if (!pyodidePromise) { pyodidePromise = loadPyodideScript() .then(() => { if (!window.loadPyodide) throw new Error("Không thể tải Pyodide.") return window.loadPyodide({ indexURL: PYODIDE_INDEX_URL }) }) .then((pyodide) => { pyodideInstance = pyodide return pyodide }) .catch((error) => { pyodidePromise = null throw error }) } return pyodidePromise } function loadPyodideScript(): Promise { if (typeof window === "undefined") return Promise.reject(new Error("Pyodide chỉ chạy trong trình duyệt.")) if (window.loadPyodide) return Promise.resolve() const existing = document.querySelector(`script[src="${PYODIDE_SCRIPT_URL}"]`) if (existing) { return new Promise((resolve, reject) => { existing.addEventListener("load", () => resolve(), { once: true }) existing.addEventListener("error", () => reject(new Error("Không thể tải pyodide.js.")), { once: true }) }) } return new Promise((resolve, reject) => { const script = document.createElement("script") script.src = PYODIDE_SCRIPT_URL script.async = true script.onload = () => resolve() script.onerror = () => reject(new Error("Không thể tải pyodide.js.")) document.head.appendChild(script) }) }