| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>MioBook Sandbox Dev Simulator</title> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <link href="https://fonts.googleapis.com/css2?family=Outfit:wght@300;400;600;800&family=JetBrains+Mono&display=swap" rel="stylesheet"> | |
| <style> | |
| body { font-family: 'Outfit', sans-serif; } | |
| code, pre { font-family: 'JetBrains Mono', monospace; } | |
| </style> | |
| </head> | |
| <body class="bg-slate-950 text-slate-100 min-h-screen flex flex-col"> | |
| <header class="border-b border-slate-800 bg-slate-900/50 backdrop-blur-md px-6 py-4 flex justify-between items-center sticky top-0 z-10"> | |
| <div class="flex items-center gap-3"> | |
| <span class="text-2xl">⚙️</span> | |
| <div> | |
| <h1 class="text-lg font-bold tracking-tight bg-gradient-to-r from-cyan-400 to-purple-500 bg-clip-text text-transparent" id="tool-title-header">Sandbox Dev Simulator</h1> | |
| <p class="text-xs text-slate-400" id="tool-desc-header">Simulate local widgets and components</p> | |
| </div> | |
| </div> | |
| <div class="flex items-center gap-3"> | |
| <button onclick="triggerDeploy()" class="bg-purple-600 hover:bg-purple-700 text-white px-3 py-1.5 rounded font-semibold text-sm transition">🚀 Deploy</button> | |
| <span class="text-xs bg-slate-800 text-slate-400 px-2 py-1 rounded font-mono border border-slate-700" id="tool-category-header">Dev Mode</span> | |
| <button onclick="reloadIframe()" class="bg-cyan-500 hover:bg-cyan-600 text-slate-950 px-3 py-1.5 rounded font-semibold text-sm transition">🔄 Reload</button> | |
| </div> | |
| </header> | |
| <main class="flex-1 flex overflow-hidden"> | |
| <!-- Left panel: Iframe --> | |
| <div class="flex-1 p-6 flex flex-col gap-4 border-r border-slate-800"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-400">Sandbox Preview</h2> | |
| <div class="flex items-center gap-2"> | |
| <span class="text-xs text-slate-500">CORS Proxy is active at /proxy</span> | |
| <span id="sandbox-status" class="text-xs px-2.5 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400 border border-yellow-500/30">loading</span> | |
| </div> | |
| </div> | |
| <div class="flex-1 bg-slate-900 border border-slate-800 rounded-xl overflow-hidden shadow-2xl relative min-h-[500px]"> | |
| <iframe id="sandbox-iframe" src="/tool.html" class="w-full h-full border-none bg-white"></iframe> | |
| </div> | |
| </div> | |
| <!-- Right panel: Controller & Console --> | |
| <div class="w-[450px] p-6 flex flex-col gap-6 overflow-y-auto bg-slate-900/30 border-l border-slate-800"> | |
| <!-- Input Section --> | |
| <div class="flex flex-col gap-3"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-400">1. Initialize Inputs</h2> | |
| <div class="flex gap-1.5"> | |
| <button onclick="toggleInputView('form')" id="btn-tab-form" class="text-[10px] px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold border border-slate-700">Form</button> | |
| <button onclick="toggleInputView('json')" id="btn-tab-json" class="text-[10px] px-2 py-1 rounded bg-slate-900 text-slate-400">JSON</button> | |
| <button onclick="toggleInputView('terminal')" id="btn-tab-terminal" class="text-[10px] px-2 py-1 rounded bg-slate-900 text-slate-400">Terminal</button> | |
| </div> | |
| </div> | |
| <!-- Form view --> | |
| <div id="input-view-form" class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-4"> | |
| <form id="dynamic-form" class="flex flex-col gap-3"> | |
| <div class="text-xs text-slate-500" id="form-fallback-msg">No parameters defined in tool.json. Use JSON tab.</div> | |
| </form> | |
| <div class="flex items-center gap-2 mt-2"> | |
| <input type="checkbox" id="auto-send-cb" checked class="rounded border-slate-800 bg-slate-950 text-cyan-500 focus:ring-0"> | |
| <label for="auto-send-cb" class="text-xs text-slate-400">Auto-send on change</label> | |
| </div> | |
| <button onclick="sendInit()" class="w-full bg-purple-600 hover:bg-purple-700 text-white py-2 rounded-lg font-semibold text-sm transition shadow-lg shadow-purple-600/20 mt-1">🚀 Send Init Payload</button> | |
| </div> | |
| <!-- JSON view (hidden by default) --> | |
| <div id="input-view-json" class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3 hidden"> | |
| <label class="text-xs text-slate-400">Mock Data Payload (JSON)</label> | |
| <textarea id="mock-payload" class="w-full h-36 bg-slate-950 border border-slate-800 rounded-lg p-3 text-xs font-mono text-cyan-400 focus:outline-none focus:border-cyan-500" placeholder='{"items": []}'>{ | |
| "title": "Monthly Target Report", | |
| "data": [12, 19, 3, 5, 2, 3] | |
| }</textarea> | |
| <button onclick="sendInit()" class="w-full bg-purple-600 hover:bg-purple-700 text-white py-2 rounded-lg font-semibold text-sm transition shadow-lg shadow-purple-600/20">🚀 Send Init Payload</button> | |
| </div> | |
| <!-- Terminal view (hidden by default) --> | |
| <div id="input-view-terminal" class="bg-slate-900 border border-slate-800 rounded-xl p-4 flex flex-col gap-3 hidden"> | |
| <div class="flex gap-2"> | |
| <input type="text" id="terminal-input" onkeydown="handleTerminalKey(event)" class="flex-1 bg-slate-950 border border-slate-800 rounded px-2.5 py-1.5 text-xs text-green-400 font-mono focus:outline-none focus:border-green-500" placeholder="Run shell command..."> | |
| <button onclick="runTerminalCommand()" class="bg-green-600 hover:bg-green-700 text-slate-950 font-bold px-3 py-1.5 rounded text-xs transition">Run</button> | |
| </div> | |
| <div id="terminal-output" class="bg-slate-950 border border-slate-800 rounded-lg p-3 h-48 font-mono text-[10px] text-green-400 overflow-y-auto whitespace-pre-wrap"> | |
| [Terminal ready] Type a command above and press Enter. | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Log Console --> | |
| <div class="flex-1 flex flex-col gap-3 min-h-[300px]"> | |
| <div class="flex justify-between items-center"> | |
| <h2 class="text-sm font-semibold uppercase tracking-wider text-slate-400">2. Sandbox Message Logs</h2> | |
| <button onclick="clearLogs()" class="text-xs text-slate-500 hover:text-slate-300">Clear</button> | |
| </div> | |
| <div id="logs-container" class="flex-1 bg-slate-950 border border-slate-800 rounded-xl p-4 overflow-y-auto flex flex-col gap-3 text-xs font-mono"> | |
| <div class="text-slate-500">[Simulator] Waiting for sandbox ready message...</div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| <!-- Modal for Deploy Result --> | |
| <div id="deploy-modal" class="fixed inset-0 bg-black/75 backdrop-blur-sm z-50 flex items-center justify-center hidden"> | |
| <div class="bg-slate-900 border border-slate-800 rounded-2xl w-[600px] max-h-[80vh] flex flex-col p-6 shadow-2xl"> | |
| <div class="flex justify-between items-center border-b border-slate-800 pb-3 mb-4"> | |
| <h3 class="text-lg font-bold text-slate-100">🚀 Deployment Output Logs</h3> | |
| <button onclick="closeDeployModal()" class="text-slate-500 hover:text-slate-300">✕</button> | |
| </div> | |
| <div class="flex-1 overflow-y-auto bg-slate-950 p-4 rounded-xl border border-slate-800 font-mono text-xs text-slate-300 whitespace-pre-wrap min-h-[200px]" id="deploy-output"> | |
| Deploying... Please wait... | |
| </div> | |
| <div class="flex justify-end mt-4"> | |
| <button onclick="closeDeployModal()" class="bg-slate-800 hover:bg-slate-700 text-slate-200 px-4 py-2 rounded-lg font-semibold text-sm transition">Close</button> | |
| </div> | |
| </div> | |
| </div> | |
| <script> | |
| const iframe = document.getElementById('sandbox-iframe'); | |
| const statusBadge = document.getElementById('sandbox-status'); | |
| const logsContainer = document.getElementById('logs-container'); | |
| let toolMetadata = null; | |
| function log(type, message, data = null) { | |
| const time = new Date().toLocaleTimeString(); | |
| const div = document.createElement('div'); | |
| div.className = 'border-l-2 pl-3 py-1 flex flex-col gap-1 '; | |
| if (type === 'Simulator') { | |
| div.className += 'border-cyan-500 text-cyan-400 bg-cyan-500/5'; | |
| } else if (type === 'ready') { | |
| div.className += 'border-green-500 text-green-400 bg-green-500/5'; | |
| } else if (type === 'submit') { | |
| div.className += 'border-purple-500 text-purple-400 bg-purple-500/5'; | |
| } else if (type.startsWith('console-')) { | |
| const level = type.split('-')[1]; | |
| if (level === 'warn') { | |
| div.className += 'border-yellow-500 text-yellow-400 bg-yellow-500/5'; | |
| div.innerHTML = '<span class="font-bold">[' + time + '] ⚠️ [WARN]:</span> <span class="whitespace-pre-wrap">' + message + '</span>'; | |
| logsContainer.appendChild(div); | |
| logsContainer.scrollTop = logsContainer.scrollHeight; | |
| return; | |
| } else if (level === 'error') { | |
| div.className += 'border-red-500 text-red-400 bg-red-500/5'; | |
| div.innerHTML = '<span class="font-bold">[' + time + '] ❌ [ERROR]:</span> <span class="whitespace-pre-wrap">' + message + '</span>'; | |
| logsContainer.appendChild(div); | |
| logsContainer.scrollTop = logsContainer.scrollHeight; | |
| return; | |
| } else { | |
| div.className += 'border-slate-500 text-slate-300 bg-slate-500/5'; | |
| div.innerHTML = '<span class="font-bold">[' + time + '] 💬 [LOG]:</span> <span class="whitespace-pre-wrap">' + message + '</span>'; | |
| logsContainer.appendChild(div); | |
| logsContainer.scrollTop = logsContainer.scrollHeight; | |
| return; | |
| } | |
| } else { | |
| div.className += 'border-yellow-500 text-yellow-400 bg-yellow-500/5'; | |
| } | |
| let dataHtml = ''; | |
| if (data) { | |
| dataHtml = '<pre class="mt-1 text-[10px] bg-black/30 p-2 rounded overflow-x-auto text-slate-300">' + JSON.stringify(data, null, 2) + '</pre>'; | |
| } | |
| div.innerHTML = '<span class="font-bold">[' + time + '] ' + type.toUpperCase() + ':</span> <span>' + message + '</span>' + dataHtml; | |
| logsContainer.appendChild(div); | |
| logsContainer.scrollTop = logsContainer.scrollHeight; | |
| } | |
| window.addEventListener('message', (event) => { | |
| const msg = event.data; | |
| if (!msg || !msg.type) return; | |
| if (msg.type === 'ready') { | |
| statusBadge.textContent = 'ready'; | |
| statusBadge.className = 'text-xs px-2.5 py-0.5 rounded-full bg-green-500/20 text-green-400 border border-green-500/30'; | |
| log('ready', 'Sandbox loaded and is ready.'); | |
| if (document.getElementById('auto-send-cb').checked) { | |
| sendInit(); | |
| } | |
| } else if (msg.type === 'submit' || msg.type === 'result') { | |
| log(msg.type, 'Received payload from sandbox:', msg.data); | |
| } else if (msg.type === 'console') { | |
| const argsStr = msg.args.map(a => typeof a === 'object' ? JSON.stringify(a, null, 2) : String(a)).join(' '); | |
| log('console-' + msg.level, argsStr); | |
| } else { | |
| log(msg.type, 'Unknown message received', msg); | |
| } | |
| }); | |
| function toggleInputView(mode) { | |
| const formView = document.getElementById('input-view-form'); | |
| const jsonView = document.getElementById('input-view-json'); | |
| const termView = document.getElementById('input-view-terminal'); | |
| const btnForm = document.getElementById('btn-tab-form'); | |
| const btnJson = document.getElementById('btn-tab-json'); | |
| const btnTerm = document.getElementById('btn-tab-terminal'); | |
| formView.classList.add('hidden'); | |
| jsonView.classList.add('hidden'); | |
| termView.classList.add('hidden'); | |
| btnForm.className = 'text-[10px] px-2 py-1 rounded bg-slate-900 text-slate-400'; | |
| btnJson.className = 'text-[10px] px-2 py-1 rounded bg-slate-900 text-slate-400'; | |
| btnTerm.className = 'text-[10px] px-2 py-1 rounded bg-slate-900 text-slate-400'; | |
| if (mode === 'form') { | |
| formView.classList.remove('hidden'); | |
| btnForm.className = 'text-[10px] px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold border border-slate-700'; | |
| } else if (mode === 'json') { | |
| jsonView.classList.remove('hidden'); | |
| btnJson.className = 'text-[10px] px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold border border-slate-700'; | |
| } else { | |
| termView.classList.remove('hidden'); | |
| btnTerm.className = 'text-[10px] px-2 py-1 rounded bg-slate-800 text-cyan-400 font-semibold border border-slate-700'; | |
| } | |
| } | |
| function sendInit() { | |
| try { | |
| let payload; | |
| if (!document.getElementById('input-view-form').classList.contains('hidden')) { | |
| payload = getFormPayload(); | |
| } else { | |
| const payloadStr = document.getElementById('mock-payload').value; | |
| payload = JSON.parse(payloadStr); | |
| } | |
| iframe.contentWindow.postMessage({ | |
| type: 'init', | |
| data: payload | |
| }, '*'); | |
| log('Simulator', 'Sent INIT payload to sandbox.', payload); | |
| } catch(e) { | |
| alert('Error: ' + e.message); | |
| } | |
| } | |
| function getFormPayload() { | |
| if (!toolMetadata || !toolMetadata.parameters) return {}; | |
| const payload = {}; | |
| toolMetadata.parameters.forEach(p => { | |
| const el = document.getElementById('param-' + p.name); | |
| if (!el) return; | |
| if (p.type === 'boolean') { | |
| payload[p.name] = el.checked; | |
| } else if (p.type === 'number') { | |
| payload[p.name] = el.value === '' ? null : Number(el.value); | |
| } else { | |
| payload[p.name] = el.value; | |
| } | |
| }); | |
| return payload; | |
| } | |
| function onFormChange() { | |
| const payload = getFormPayload(); | |
| document.getElementById('mock-payload').value = JSON.stringify(payload, null, 2); | |
| if (document.getElementById('auto-send-cb').checked) { | |
| iframe.contentWindow.postMessage({ | |
| type: 'init', | |
| data: payload | |
| }, '*'); | |
| } | |
| } | |
| function reloadIframe() { | |
| iframe.src = iframe.src; | |
| statusBadge.textContent = 'loading'; | |
| statusBadge.className = 'text-xs px-2.5 py-0.5 rounded-full bg-yellow-500/20 text-yellow-400 border border-yellow-500/30'; | |
| log('Simulator', 'Iframe reloaded.'); | |
| } | |
| function clearLogs() { | |
| logsContainer.innerHTML = '<div class="text-slate-500">[Simulator] Logs cleared.</div>'; | |
| } | |
| function triggerDeploy() { | |
| const modal = document.getElementById('deploy-modal'); | |
| const output = document.getElementById('deploy-output'); | |
| modal.classList.remove('hidden'); | |
| output.textContent = 'Deploying tool using local SDK deploy logic... Please wait...'; | |
| fetch('/api/deploy', { method: 'POST' }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| if (data.status === 'success') { | |
| output.innerHTML = '<span class="text-green-400 font-bold">✔ SUCCESS</span>\n\n' + data.output; | |
| } else { | |
| output.innerHTML = '<span class="text-red-400 font-bold">✘ ERROR</span>\n\n' + data.output; | |
| } | |
| }) | |
| .catch(err => { | |
| output.innerHTML = '<span class="text-red-400 font-bold">✘ NETWORK ERROR:</span> ' + err.message; | |
| }); | |
| } | |
| function closeDeployModal() { | |
| document.getElementById('deploy-modal').classList.add('hidden'); | |
| } | |
| function runTerminalCommand() { | |
| const input = document.getElementById('terminal-input'); | |
| const output = document.getElementById('terminal-output'); | |
| const cmd = input.value.trim(); | |
| if (!cmd) return; | |
| output.textContent = '$ ' + cmd + '\nRunning...'; | |
| fetch('/api/terminal', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ command: cmd }) | |
| }) | |
| .then(r => r.json()) | |
| .then(data => { | |
| output.textContent = '$ ' + cmd + '\n' + data.output; | |
| output.scrollTop = output.scrollHeight; | |
| }) | |
| .catch(err => { | |
| output.textContent = '$ ' + cmd + '\nNetwork Error: ' + err.message; | |
| }); | |
| } | |
| function handleTerminalKey(e) { | |
| if (e.key === 'Enter') { | |
| runTerminalCommand(); | |
| } | |
| } | |
| fetch('/tool.json') | |
| .then(r => r.json()) | |
| .then(data => { | |
| toolMetadata = data; | |
| document.getElementById('tool-title-header').textContent = data.name || 'Sandbox Dev Simulator'; | |
| document.getElementById('tool-desc-header').textContent = data.description || 'Simulate local widgets and components'; | |
| document.getElementById('tool-category-header').textContent = (data.category || 'dev').toUpperCase(); | |
| if (data.parameters && data.parameters.length > 0) { | |
| const form = document.getElementById('dynamic-form'); | |
| form.innerHTML = ''; | |
| data.parameters.forEach(p => { | |
| const row = document.createElement('div'); | |
| row.className = 'flex flex-col gap-1'; | |
| const label = document.createElement('label'); | |
| label.className = 'text-xs text-slate-400 font-semibold'; | |
| label.textContent = p.name + (p.required ? ' *' : ''); | |
| let input; | |
| if (p.type === 'boolean') { | |
| row.className = 'flex items-center justify-between py-1 border-b border-slate-800'; | |
| input = document.createElement('input'); | |
| input.type = 'checkbox'; | |
| input.className = 'rounded border-slate-800 bg-slate-950 text-cyan-500 focus:ring-0'; | |
| } else if (p.type === 'number') { | |
| input = document.createElement('input'); | |
| input.type = 'number'; | |
| input.className = 'w-full bg-slate-950 border border-slate-800 rounded px-3 py-1.5 text-xs text-cyan-400 focus:outline-none focus:border-cyan-500'; | |
| } else { | |
| input = document.createElement('input'); | |
| input.type = 'text'; | |
| input.className = 'w-full bg-slate-950 border border-slate-800 rounded px-3 py-1.5 text-xs text-cyan-400 focus:outline-none focus:border-cyan-500'; | |
| } | |
| input.id = 'param-' + p.name; | |
| input.onchange = onFormChange; | |
| input.oninput = onFormChange; | |
| if (p.type === 'boolean') { | |
| row.appendChild(label); | |
| row.appendChild(input); | |
| } else { | |
| row.appendChild(label); | |
| row.appendChild(input); | |
| if (p.description) { | |
| const desc = document.createElement('span'); | |
| desc.className = 'text-[10px] text-slate-500'; | |
| desc.textContent = p.description; | |
| row.appendChild(desc); | |
| } | |
| } | |
| form.appendChild(row); | |
| }); | |
| onFormChange(); | |
| } | |
| }) | |
| .catch(e => { | |
| console.log('No tool.json found or failed to load:', e); | |
| }); | |
| const devSource = new EventSource('/live-reload'); | |
| devSource.onmessage = function(e) { | |
| if (e.data === 'reload') { | |
| console.log('[Dev Simulator] Reload event received, reloading iframe...'); | |
| reloadIframe(); | |
| } | |
| }; | |
| </script> | |
| </body> | |
| </html> | |
Xet Storage Details
- Size:
- 19.5 kB
- Xet hash:
- 991ce4200c635c8f6a8b00916f18916649048c8b2d1a1fe72f0c6d84c92cde40
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.