Files
homeclaw/web-ui/html/python-runner.html
T

149 lines
5.7 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Python Runner</title>
<style>
@import url('https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500;600&display=swap');
:root { --bg: #0c0c0c; --panel: #1a1a2e; --accent: #3b82f6; --green: #22c55e; --red: #ef4444; --muted: #6b7280; }
* { margin:0; padding:0; box-sizing:border-box; }
body { background:var(--bg); color:#e5e7eb; font-family:'IBM Plex Mono',monospace; height:100vh; display:flex; flex-direction:column; }
.header { display:flex; align-items:center; gap:12px; padding:10px 16px; background:var(--panel); border-bottom:1px solid #2d2d4a; flex-shrink:0; }
.header h1 { font-size:14px; font-weight:600; color:#e5e7eb; }
.header .file { font-size:12px; color:var(--accent); }
.status { font-size:11px; padding:2px 8px; border-radius:4px; font-weight:600; }
.status.loading { background:#1e3a5f; color:#60a5fa; }
.status.ready { background:#1a3a2a; color:var(--green); }
.status.running { background:#3a3a1a; color:#fbbf24; }
.status.error { background:#3a1a1a; color:var(--red); }
.toolbar { display:flex; gap:8px; margin-left:auto; }
.toolbar button { background:#252540; border:1px solid #3a3a5a; color:#e5e7eb; border-radius:6px; padding:6px 14px; font-size:12px; cursor:pointer; font-family:inherit; transition:all .15s; }
.toolbar button:hover { background:#353555; border-color:#4a4a7a; }
.toolbar button:disabled { opacity:.4; cursor:not-allowed; }
.toolbar button.run { background:#1a3a2a; border-color:#2d5a3a; color:var(--green); }
.toolbar button.run:hover { background:#224a3a; }
.output { flex:1; overflow:auto; padding:16px; font-size:13px; line-height:1.6; white-space:pre-wrap; word-break:break-all; }
.output .stdout { color:#e5e7eb; }
.output .stderr { color:var(--red); }
.output .info { color:var(--muted); font-style:italic; }
.footer { padding:8px 16px; border-top:1px solid #2d2d4a; font-size:11px; color:var(--muted); background:var(--panel); flex-shrink:0; display:flex; justify-content:space-between; }
</style>
</head>
<body>
<div class="header">
<h1>🐍 Python</h1>
<span class="file" id="filename"></span>
<span class="status loading" id="status">Pyodide 로딩중...</span>
<div class="toolbar">
<button class="run" id="btn-run" disabled onclick="runCode()">▶ 실행</button>
</div>
</div>
<div class="output" id="output"><span class="info">Pyodide를 로딩하고 있습니다...</span></div>
<div class="footer">
<span id="footer-info">Pyodide (WebAssembly Python)</span>
<span id="exec-time"></span>
</div>
<script src="https://cdn.jsdelivr.net/pyodide/v0.27.3/full/pyodide.js"></script>
<script>
let pyodide = null;
let code = sessionStorage.getItem('py_run_code') || '';
const fileName = sessionStorage.getItem('py_run_name') || 'script.py';
sessionStorage.removeItem('py_run_code');
sessionStorage.removeItem('py_run_name');
document.getElementById('filename').textContent = fileName;
document.title = fileName + ' — Python Runner';
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + cls;
}
function appendOutput(text, cls = 'stdout') {
const el = document.getElementById('output');
const span = document.createElement('span');
span.className = cls;
span.textContent = text;
el.appendChild(span);
el.scrollTop = el.scrollHeight;
}
function clearOutput() {
document.getElementById('output').innerHTML = '';
}
async function initPyodide() {
try {
pyodide = await loadPyodide();
setStatus('준비', 'ready');
document.getElementById('btn-run').disabled = false;
const pyVer = pyodide.runPython('import sys; sys.version.split()[0]');
document.getElementById('footer-info').textContent = `Pyodide ${pyodide.version} (Python ${pyVer})`;
clearOutput();
if (code) {
appendOutput(`${fileName} 로드 완료 (${(new TextEncoder().encode(code).length / 1024).toFixed(1)}KB). ▶ 실행을 누르세요.\n`, 'info');
} else {
appendOutput('코드가 전달되지 않았습니다.\n', 'stderr');
setStatus('코드 없음', 'error');
}
} catch (e) {
setStatus('로딩 실패', 'error');
clearOutput();
appendOutput('Pyodide 로딩 실패:\n' + (e.stack || e.message || e) + '\n', 'stderr');
}
}
async function runCode() {
if (!pyodide) return;
if (!code) { appendOutput('실행할 코드가 없습니다.\n', 'stderr'); return; }
document.getElementById('btn-run').disabled = true;
clearOutput();
setStatus('실행중', 'running');
const t0 = performance.now();
try {
// Capture stdout/stderr
pyodide.runPython(`
import sys, io
_stdout_capture = io.StringIO()
_stderr_capture = io.StringIO()
sys.stdout = _stdout_capture
sys.stderr = _stderr_capture
`);
pyodide.runPython(code);
const stdout = pyodide.runPython('_stdout_capture.getvalue()');
const stderr = pyodide.runPython('_stderr_capture.getvalue()');
if (stdout) appendOutput(stdout);
if (stderr) appendOutput(stderr, 'stderr');
setStatus('완료', 'ready');
} catch (e) {
// Show full Python traceback
const tb = e.stack || e.message || String(e);
appendOutput(tb + '\n', 'stderr');
setStatus('오류', 'error');
} finally {
// Safely restore stdout
try {
pyodide.runPython(`
try:
sys.stdout = sys.__stdout__
sys.stderr = sys.__stderr__
except:
sys.stdout = io.StringIO()
sys.stderr = io.StringIO()
`);
} catch {}
const elapsed = ((performance.now() - t0) / 1000).toFixed(2);
document.getElementById('exec-time').textContent = `${elapsed}s`;
document.getElementById('btn-run').disabled = false;
}
}
initPyodide();
</script>
</body>
</html>