From 062dabfb9884c87ee77b991cc3c0abc326216081 Mon Sep 17 00:00:00 2001 From: orereoo66 <130591461+orereoo66@users.noreply.github.com> Date: Sun, 12 Oct 2025 00:16:12 +0900 Subject: [PATCH 1/4] Add Python playground page with Pyodide runtime --- src/pages/python-playground.tsx | 269 ++++++++++++++++++++++++++++++++ 1 file changed, 269 insertions(+) create mode 100644 src/pages/python-playground.tsx diff --git a/src/pages/python-playground.tsx b/src/pages/python-playground.tsx new file mode 100644 index 0000000000..1e4984f0f5 --- /dev/null +++ b/src/pages/python-playground.tsx @@ -0,0 +1,269 @@ +import { type NextPage } from "next"; +import Head from "next/head"; +import { useCallback, useEffect, useMemo, useState } from "react"; +import Button from "../components/Button"; +import DefaultLayout from "../layout/default"; + +const PYODIDE_VERSION = "0.24.1"; +const DEFAULT_CODE = `import numpy as np +import matplotlib.pyplot as plt + +x = np.linspace(0, 2 * np.pi, 200) +y = np.sin(x) + +plt.figure(figsize=(6, 3)) +plt.plot(x, y) +plt.title("Sine wave") +plt.xlabel("x") +plt.ylabel("sin(x)") +plt.grid(True) +plt.show() +`; + +declare global { + interface Window { + loadPyodide?: (options: { indexURL: string }) => Promise; + } +} + +const PythonPlaygroundPage: NextPage = () => { + const [pyodide, setPyodide] = useState(null); + const [code, setCode] = useState(DEFAULT_CODE); + const [output, setOutput] = useState(""); + const [images, setImages] = useState([]); + const [isLoading, setIsLoading] = useState(true); + const [statusMessage, setStatusMessage] = useState("Python環境を読み込み中..."); + const [isRunning, setIsRunning] = useState(false); + const [errorMessage, setErrorMessage] = useState(null); + + useEffect(() => { + let cancelled = false; + + const loadPyodideRuntime = async () => { + if (typeof window === "undefined") { + return; + } + + try { + setStatusMessage("Pyodideをダウンロード中..."); + if (!window.loadPyodide) { + await new Promise((resolve, reject) => { + const script = document.createElement("script"); + script.src = `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/pyodide.js`; + script.onload = () => resolve(); + script.onerror = () => reject(new Error("Pyodideの読み込みに失敗しました")); + document.body.appendChild(script); + }); + } + + if (!window.loadPyodide) { + throw new Error("Pyodideが利用できません"); + } + + setStatusMessage("Python環境を初期化しています..."); + const instance = await window.loadPyodide({ + indexURL: `https://cdn.jsdelivr.net/pyodide/v${PYODIDE_VERSION}/full/`, + }); + + if (cancelled) { + return; + } + + setStatusMessage("科学計算ライブラリを読み込み中..."); + await instance.loadPackage(["numpy", "matplotlib"]); + + if (cancelled) { + return; + } + + setPyodide(instance); + setIsLoading(false); + setStatusMessage("Python環境の準備が整いました!"); + } catch (error) { + if (!cancelled) { + const message = + error instanceof Error ? error.message : "不明なエラーが発生しました"; + setErrorMessage(message); + setIsLoading(false); + } + } + }; + + void loadPyodideRuntime(); + + return () => { + cancelled = true; + }; + }, []); + + const runCode = useCallback(async () => { + if (!pyodide) { + return; + } + + setIsRunning(true); + setImages([]); + setOutput(""); + setErrorMessage(null); + + const pushFigure = (img: unknown) => { + if (typeof img === "string") { + setImages((prev) => [...prev, img]); + } + }; + + pyodide.globals.set("send_figure", pushFigure); + + try { + const result = await pyodide.runPythonAsync(` +import sys +import io +import base64 + +from js import send_figure + +_stdout = sys.stdout +_stderr = sys.stderr +_buffer = io.StringIO() +sys.stdout = _buffer +sys.stderr = _buffer + +import matplotlib +matplotlib.use("AGG") +from matplotlib import pyplot as plt + +import io as _img_io + +def _flush_figures(): + figs = plt.get_fignums() + for fig in figs: + plt.figure(fig) + buf = _img_io.BytesIO() + plt.savefig(buf, format="png", bbox_inches="tight") + buf.seek(0) + send_figure("data:image/png;base64," + base64.b64encode(buf.read()).decode("ascii")) + buf.close() + plt.close(fig) + +def show(*args, **kwargs): + _flush_figures() + +plt.show = show + +${code} + +_flush_figures() +sys.stdout = _stdout +sys.stderr = _stderr +_buffer.getvalue() +`); + + const textOutput = + typeof result === "string" + ? result + : typeof result?.toString === "function" + ? result.toString() + : ""; + + setOutput(textOutput); + + if (result && typeof (result as { destroy?: () => void }).destroy === "function") { + (result as { destroy: () => void }).destroy(); + } + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + setErrorMessage(message); + } finally { + setIsRunning(false); + } + }, [code, pyodide]); + + const pageTitle = useMemo( + () => "Python Playground | AgentGPT", + [] + ); + + return ( + + + {pageTitle} + +
+
+
+

Pythonプレイグラウンド

+

+ ブラウザ上でPythonコードを実行して、その場でグラフを確認できます。NumPyとMatplotlibは既にインストール済みです。 +

+
+ +
+
+ +