mirror of
https://github.com/openai/codex.git
synced 2026-10-24 18:18:41 +00:00
Upgrade the wasm harness from a one-shot sampler to a real Responses API loop that uses the Codex prompt, handles function_call / function_call_output turns, and exposes a browser exec_js callback. The example page now provisions a sandboxed iframe kernel, persists the API key in localStorage, surfaces browser errors inline, and has been verified in Chrome Testing with a live prompt that executes JavaScript and completes the turn.
412 lines
11 KiB
HTML
412 lines
11 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Codex WASM Harness Prototype</title>
|
|
<style>
|
|
:root {
|
|
color-scheme: light;
|
|
font-family:
|
|
ui-serif, "Iowan Old Style", "Palatino Linotype", Georgia, serif;
|
|
background: #f2eddf;
|
|
color: #1f2a24;
|
|
}
|
|
|
|
body {
|
|
margin: 0;
|
|
min-height: 100vh;
|
|
background:
|
|
radial-gradient(circle at 20% 10%, #ffc96b 0 9rem, transparent 9rem),
|
|
linear-gradient(135deg, #f8f2e3 0%, #dfecdc 48%, #c7dde7 100%);
|
|
}
|
|
|
|
main {
|
|
box-sizing: border-box;
|
|
max-width: 960px;
|
|
margin: 0 auto;
|
|
padding: 48px 20px;
|
|
}
|
|
|
|
h1 {
|
|
max-width: 760px;
|
|
margin: 0 0 12px;
|
|
font-size: clamp(2.4rem, 7vw, 5.5rem);
|
|
line-height: 0.92;
|
|
letter-spacing: -0.06em;
|
|
}
|
|
|
|
p {
|
|
max-width: 720px;
|
|
font-size: 1.1rem;
|
|
}
|
|
|
|
form {
|
|
display: grid;
|
|
grid-template-columns: 1fr auto;
|
|
gap: 12px;
|
|
margin: 32px 0;
|
|
}
|
|
|
|
input,
|
|
button {
|
|
border: 2px solid #1f2a24;
|
|
border-radius: 999px;
|
|
font: inherit;
|
|
font-size: 1.05rem;
|
|
padding: 14px 18px;
|
|
}
|
|
|
|
input {
|
|
background: #fffaf0;
|
|
}
|
|
|
|
button {
|
|
background: #1f2a24;
|
|
color: #fffaf0;
|
|
cursor: pointer;
|
|
}
|
|
|
|
label {
|
|
display: grid;
|
|
gap: 6px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
.api-key-panel {
|
|
display: grid;
|
|
grid-template-columns: minmax(0, 1fr) auto;
|
|
gap: 14px;
|
|
align-items: end;
|
|
margin: 28px 0 0;
|
|
border: 2px solid rgb(31 42 36 / 0.26);
|
|
border-radius: 28px;
|
|
background: rgb(255 250 240 / 0.68);
|
|
padding: 18px;
|
|
}
|
|
|
|
.api-key-panel h2 {
|
|
margin: 0 0 8px;
|
|
font-size: 1.1rem;
|
|
letter-spacing: -0.03em;
|
|
}
|
|
|
|
.api-key-panel p {
|
|
margin: 0;
|
|
font-size: 0.95rem;
|
|
}
|
|
|
|
.api-key-panel label {
|
|
grid-column: 1 / -1;
|
|
}
|
|
|
|
.api-key-actions {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
grid-column: 1 / -1;
|
|
gap: 10px;
|
|
align-items: center;
|
|
}
|
|
|
|
.secondary-button {
|
|
background: transparent;
|
|
color: #1f2a24;
|
|
}
|
|
|
|
.api-key-status {
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.85rem;
|
|
}
|
|
|
|
#events {
|
|
display: grid;
|
|
gap: 12px;
|
|
}
|
|
|
|
.event {
|
|
border: 2px solid rgb(31 42 36 / 0.22);
|
|
border-radius: 20px;
|
|
background: rgb(255 250 240 / 0.75);
|
|
padding: 14px 16px;
|
|
box-shadow: 0 16px 40px rgb(31 42 36 / 0.08);
|
|
}
|
|
|
|
.event strong {
|
|
display: block;
|
|
margin-bottom: 6px;
|
|
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
font-size: 0.82rem;
|
|
letter-spacing: 0.08em;
|
|
text-transform: uppercase;
|
|
}
|
|
|
|
pre {
|
|
margin: 0;
|
|
overflow: auto;
|
|
white-space: pre-wrap;
|
|
}
|
|
|
|
@media (max-width: 640px) {
|
|
form,
|
|
.api-key-panel {
|
|
grid-template-columns: 1fr;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<main>
|
|
<h1>Codex WASM harness prototype</h1>
|
|
<p>
|
|
This page exercises the browser boundary: submit a prompt, stream
|
|
Codex-shaped events from WASM, and resolve when the turn completes.
|
|
</p>
|
|
<section class="api-key-panel" aria-labelledby="api-key-title">
|
|
<div>
|
|
<h2 id="api-key-title">Responses API key</h2>
|
|
<p>
|
|
Local prototype only. The key is stored in this browser's
|
|
localStorage and is sent directly from this page to the Responses
|
|
API when you run a turn.
|
|
</p>
|
|
</div>
|
|
<label>
|
|
API key
|
|
<input
|
|
id="api-key"
|
|
type="password"
|
|
placeholder="sk-..."
|
|
autocomplete="off"
|
|
spellcheck="false"
|
|
/>
|
|
</label>
|
|
<div class="api-key-actions">
|
|
<button id="clear-api-key" class="secondary-button" type="button">
|
|
Forget key
|
|
</button>
|
|
<output id="api-key-status" class="api-key-status"></output>
|
|
</div>
|
|
</section>
|
|
<form id="prompt-form">
|
|
<input
|
|
id="prompt"
|
|
value="write hello world in JavaScript, execute it with exec_js, and tell me the result"
|
|
autocomplete="off"
|
|
/>
|
|
<button type="submit">Run turn</button>
|
|
</form>
|
|
<section id="events" aria-live="polite"></section>
|
|
</main>
|
|
<script type="module">
|
|
import init, { BrowserCodex } from "../pkg/codex_wasm_harness.js";
|
|
|
|
await init();
|
|
|
|
const form = document.querySelector("#prompt-form");
|
|
const input = document.querySelector("#prompt");
|
|
const events = document.querySelector("#events");
|
|
const apiKeyInput = document.querySelector("#api-key");
|
|
const apiKeyStatus = document.querySelector("#api-key-status");
|
|
const clearApiKeyButton = document.querySelector("#clear-api-key");
|
|
const API_KEY_STORAGE_KEY = "codexWasmHarness.apiKey";
|
|
const codex = new BrowserCodex(readStoredApiKey());
|
|
const sandbox = createSandboxKernel();
|
|
|
|
apiKeyInput.value = readStoredApiKey();
|
|
updateApiKeyStatus();
|
|
codex.set_code_executor(async (code) => sandbox.execute(code));
|
|
|
|
apiKeyInput.addEventListener("input", () => {
|
|
const apiKey = apiKeyInput.value.trim();
|
|
writeStoredApiKey(apiKey);
|
|
codex.set_api_key(apiKey);
|
|
updateApiKeyStatus();
|
|
});
|
|
|
|
clearApiKeyButton.addEventListener("click", () => {
|
|
apiKeyInput.value = "";
|
|
writeStoredApiKey("");
|
|
codex.set_api_key("");
|
|
updateApiKeyStatus();
|
|
});
|
|
|
|
form.addEventListener("submit", async (event) => {
|
|
event.preventDefault();
|
|
events.replaceChildren();
|
|
try {
|
|
await codex.submit_turn(input.value, renderEvent);
|
|
} catch (error) {
|
|
renderEvent({
|
|
type: "page_error",
|
|
message: error instanceof Error ? error.message : String(error),
|
|
});
|
|
}
|
|
});
|
|
|
|
window.addEventListener("error", (event) => {
|
|
renderEvent({
|
|
type: "window_error",
|
|
message: event.message,
|
|
});
|
|
});
|
|
|
|
window.addEventListener("unhandledrejection", (event) => {
|
|
renderEvent({
|
|
type: "unhandled_rejection",
|
|
message:
|
|
event.reason instanceof Error
|
|
? event.reason.message
|
|
: String(event.reason),
|
|
});
|
|
});
|
|
|
|
function readStoredApiKey() {
|
|
try {
|
|
return localStorage.getItem(API_KEY_STORAGE_KEY) ?? "";
|
|
} catch {
|
|
return "";
|
|
}
|
|
}
|
|
|
|
function writeStoredApiKey(apiKey) {
|
|
try {
|
|
if (apiKey) {
|
|
localStorage.setItem(API_KEY_STORAGE_KEY, apiKey);
|
|
} else {
|
|
localStorage.removeItem(API_KEY_STORAGE_KEY);
|
|
}
|
|
} catch {
|
|
apiKeyStatus.textContent =
|
|
"localStorage is unavailable in this browser context.";
|
|
}
|
|
}
|
|
|
|
function updateApiKeyStatus() {
|
|
apiKeyStatus.textContent = apiKeyInput.value
|
|
? "API key stored in localStorage for this browser."
|
|
: "No API key stored. The deterministic demo sampler will be used.";
|
|
}
|
|
|
|
function renderEvent(event) {
|
|
const card = document.createElement("article");
|
|
card.className = "event";
|
|
const title = document.createElement("strong");
|
|
title.textContent = event.type;
|
|
const body = document.createElement("pre");
|
|
body.textContent = JSON.stringify(event, null, 2);
|
|
card.append(title, body);
|
|
events.append(card);
|
|
}
|
|
|
|
function createSandboxKernel() {
|
|
const iframe = document.createElement("iframe");
|
|
iframe.setAttribute("sandbox", "allow-scripts allow-same-origin");
|
|
iframe.style.display = "none";
|
|
iframe.srcdoc = `
|
|
<!doctype html>
|
|
<html>
|
|
<body>
|
|
<script>
|
|
function formatValue(value) {
|
|
if (typeof value === "string") {
|
|
return value;
|
|
}
|
|
if (value === undefined || value === null) {
|
|
return "";
|
|
}
|
|
try {
|
|
return JSON.stringify(value);
|
|
} catch {
|
|
return String(value);
|
|
}
|
|
}
|
|
|
|
window.addEventListener("message", async (event) => {
|
|
const data = event.data;
|
|
if (!data || data.type !== "exec_js") {
|
|
return;
|
|
}
|
|
|
|
const logs = [];
|
|
const consoleProxy = {
|
|
log: (...args) => logs.push(args.map(formatValue).join(" ")),
|
|
error: (...args) => logs.push(args.map(formatValue).join(" ")),
|
|
};
|
|
|
|
try {
|
|
const runner = new Function(
|
|
"console",
|
|
"return (async () => {\\n" + data.code + "\\n})();",
|
|
);
|
|
const result = await runner(consoleProxy);
|
|
const outputParts = logs.filter(Boolean);
|
|
const formattedResult = formatValue(result);
|
|
if (formattedResult) {
|
|
outputParts.push(formattedResult);
|
|
}
|
|
parent.postMessage(
|
|
{
|
|
type: "exec_js_result",
|
|
requestId: data.requestId,
|
|
ok: true,
|
|
output: outputParts.join("\\n"),
|
|
},
|
|
"*",
|
|
);
|
|
} catch (error) {
|
|
parent.postMessage(
|
|
{
|
|
type: "exec_js_result",
|
|
requestId: data.requestId,
|
|
ok: false,
|
|
error: error instanceof Error ? error.message : String(error),
|
|
},
|
|
"*",
|
|
);
|
|
}
|
|
});
|
|
<\/script>
|
|
</body>
|
|
</html>`;
|
|
document.body.append(iframe);
|
|
|
|
let nextRequestId = 0;
|
|
const pending = new Map();
|
|
|
|
window.addEventListener("message", (event) => {
|
|
const data = event.data;
|
|
if (!data || data.type !== "exec_js_result") {
|
|
return;
|
|
}
|
|
const entry = pending.get(data.requestId);
|
|
if (!entry) {
|
|
return;
|
|
}
|
|
pending.delete(data.requestId);
|
|
if (data.ok) {
|
|
entry.resolve(data.output ?? "");
|
|
} else {
|
|
entry.reject(new Error(data.error ?? "sandbox execution failed"));
|
|
}
|
|
});
|
|
|
|
return {
|
|
execute(code) {
|
|
return new Promise((resolve, reject) => {
|
|
nextRequestId += 1;
|
|
pending.set(nextRequestId, { resolve, reject });
|
|
iframe.contentWindow.postMessage(
|
|
{ type: "exec_js", requestId: nextRequestId, code },
|
|
"*",
|
|
);
|
|
});
|
|
},
|
|
};
|
|
}
|
|
</script>
|
|
</body>
|
|
</html>
|