driftshield / support_ops_env /server /play_console.html
raj921
feat(ui): DriftShield /play HumanAgent + State observer console
e250ada
Raw
History Blame Contribute Delete
10.7 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>DriftShield — Human play</title>
<style>
* { box-sizing: border-box; margin: 0; padding: 0; }
body {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
height: 100vh;
overflow: hidden;
color: #1a1a1a;
}
.container { display: flex; height: 100vh; }
.left {
width: 50%;
background: #fff;
border-right: 1px solid #e0e0e0;
display: flex;
flex-direction: column;
}
.right {
width: 50%;
background: #f7f7f7;
display: flex;
flex-direction: column;
}
.pane-header {
padding: 16px 20px;
border-bottom: 1px solid #e0e0e0;
background: #f8f9fa;
font-weight: 600;
font-size: 15px;
}
.pane-content { flex: 1; padding: 20px; overflow-y: auto; }
.card {
background: #fff;
border: 1px solid #e0e0e0;
border-radius: 6px;
padding: 16px;
margin-bottom: 16px;
}
.card h3 { font-size: 13px; color: #555; margin-bottom: 10px; }
label { display: block; font-weight: 500; font-size: 14px; margin-bottom: 6px; }
.req { color: #c00; }
textarea {
width: 100%;
min-height: 88px;
padding: 10px 12px;
border: 1px solid #ccc;
border-radius: 4px;
font-size: 14px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
resize: vertical;
}
textarea:focus {
outline: none;
border-color: #0d6efd;
box-shadow: 0 0 0 2px rgba(13, 110, 253, 0.2);
}
.hint { font-size: 12px; color: #666; margin-top: 6px; line-height: 1.4; }
.row-btns { margin-top: 12px; display: flex; flex-wrap: wrap; gap: 10px; align-items: center; }
.btn {
border: none;
padding: 10px 20px;
border-radius: 4px;
font-size: 14px;
cursor: pointer;
font-weight: 500;
}
.btn-primary { background: #0d6efd; color: #fff; }
.btn-primary:hover { background: #0b5ed7; }
.btn-primary:disabled { background: #6c757d; cursor: not-allowed; }
.btn-secondary { background: #6c757d; color: #fff; }
.btn-secondary:hover { background: #5c636a; }
.state-row { display: flex; justify-content: space-between; margin-bottom: 8px; font-size: 14px; }
.state-label { color: #666; }
.state-value { font-family: ui-monospace, monospace; color: #111; }
.obs-box, .hist-box {
background: #fafafa;
border: 1px solid #e8e8e8;
border-radius: 4px;
padding: 12px;
min-height: 120px;
max-height: 38vh;
overflow: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace;
font-size: 12px;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
}
.empty { color: #999; font-style: italic; }
.log-item { border-bottom: 1px solid #eee; padding: 10px 0; }
.log-item:last-child { border-bottom: none; }
.log-meta { font-size: 11px; color: #888; margin-bottom: 4px; }
.err { color: #b00020; font-size: 13px; margin-top: 8px; }
a.toplink { font-size: 12px; color: #0d6efd; }
</style>
</head>
<body>
<div class="container">
<div class="left">
<div class="pane-header">HumanAgent Interface</div>
<div class="pane-content">
<div class="card">
<h3>Take action</h3>
<label for="cmd">Command <span class="req">*</span></label>
<textarea id="cmd" placeholder="Enter assistant message, or full JSON action…"></textarea>
<p class="hint">
Plain text → <code>assistant_message</code> (empty <code>tool_calls</code>).<br />
JSON → <code>{"assistant_message":"…","tool_calls":[]}</code> or a single tool
<code>{"name":"billing.get_invoice","args":{}}</code>.
</p>
<div class="row-btns">
<button type="button" class="btn btn-primary" id="btnStep">Step</button>
<button type="button" class="btn btn-secondary" id="btnReset">Reset environment</button>
<button type="button" class="btn btn-secondary" id="btnState">Get state</button>
</div>
<p class="err" id="err" style="display:none"></p>
</div>
<div class="card">
<h3>Current state</h3>
<div class="state-row"><span class="state-label">Status</span><span class="state-value" id="stStatus"></span></div>
<div class="state-row"><span class="state-label">Episode ID</span><span class="state-value" id="stEp"></span></div>
<div class="state-row"><span class="state-label">Task ID</span><span class="state-value" id="stTask"></span></div>
<div class="state-row"><span class="state-label">Step count</span><span class="state-value" id="stSteps"></span></div>
</div>
<p><a class="toplink" href="/web">Open full OpenEnv form (advanced)</a> · <a class="toplink" href="/">API JSON</a></p>
</div>
</div>
<div class="right">
<div class="pane-header">State observer</div>
<div class="pane-content">
<div class="card">
<h3>Current observation</h3>
<div class="obs-box" id="obsBox"><span class="empty">No observation yet</span></div>
</div>
<div class="card">
<h3>Action history</h3>
<div class="hist-box" id="histBox"><span class="empty">No actions taken yet</span></div>
</div>
</div>
</div>
</div>
<script>
function parseAction(raw) {
const s = (raw || "").trim();
if (!s) return { assistant_message: "", tool_calls: [] };
if (s.startsWith("{")) {
try {
const o = JSON.parse(s);
if (o.assistant_message !== undefined || o.tool_calls !== undefined) {
return {
assistant_message: o.assistant_message != null ? String(o.assistant_message) : "",
tool_calls: Array.isArray(o.tool_calls) ? o.tool_calls : [],
};
}
if (o.name !== undefined) {
return { assistant_message: "", tool_calls: [o] };
}
} catch (e) {
return { assistant_message: s, tool_calls: [] };
}
}
return { assistant_message: s, tool_calls: [] };
}
const errEl = document.getElementById("err");
function setErr(msg) {
if (msg) {
errEl.style.display = "block";
errEl.textContent = msg;
} else {
errEl.style.display = "none";
errEl.textContent = "";
}
}
const hist = [];
function addHistory(line) {
hist.push(line);
const box = document.getElementById("histBox");
box.classList.remove("empty");
box.innerHTML = hist
.map(
(h) =>
`<div class="log-item"><div class="log-meta">${escapeHtml(
h.t
)}</div><div>${escapeHtml(h.a)}</div><div class="log-meta" style="margin-top:6px">reward: ${h.r != null ? h.r : "—"} · done: ${h.d}</div></div>`
)
.join("");
}
function escapeHtml(s) {
if (s == null) return "";
return String(s)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;");
}
function showObs(j) {
const box = document.getElementById("obsBox");
const o = j && (j.observation != null ? j.observation : j);
if (!o || (typeof o === "object" && !Object.keys(o).length)) {
box.innerHTML = '<span class="empty">No observation yet</span>';
return;
}
box.classList.remove("empty");
const payload = {
reward: j.reward,
done: j.done,
observation: o,
};
box.textContent = JSON.stringify(payload, null, 2);
}
function updateStateFromServer(st) {
if (!st) return;
document.getElementById("stEp").textContent = st.episode_id || "—";
document.getElementById("stTask").textContent = st.task_id || "—";
document.getElementById("stSteps").textContent =
st.step_count != null ? String(st.step_count) : "—";
}
document.getElementById("btnReset").addEventListener("click", async () => {
setErr("");
try {
const r = await fetch("/web/reset", { method: "POST" });
const t = await r.text();
let j;
try {
j = JSON.parse(t);
} catch (e) {
setErr("Reset failed: " + t.slice(0, 200));
return;
}
if (!r.ok) {
setErr("Reset HTTP " + r.status + " — " + t.slice(0, 300));
return;
}
hist.length = 0;
document.getElementById("histBox").innerHTML =
'<span class="empty">No actions taken yet</span>';
document.getElementById("stStatus").textContent = "Reset";
showObs(j);
await refreshState();
} catch (e) {
setErr(String(e));
}
});
document.getElementById("btnStep").addEventListener("click", async () => {
setErr("");
const action = parseAction(document.getElementById("cmd").value);
try {
const r = await fetch("/web/step", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action }),
});
const t = await r.text();
let j;
try {
j = JSON.parse(t);
} catch (e) {
setErr("Step failed: " + t.slice(0, 200));
return;
}
if (!r.ok) {
setErr("Step HTTP " + r.status + " — " + t.slice(0, 300));
return;
}
document.getElementById("stStatus").textContent = j.done ? "Done" : "Stepped";
showObs(j);
addHistory({
t: new Date().toISOString(),
a: JSON.stringify(action),
r: j.reward,
d: !!j.done,
});
await refreshState();
} catch (e) {
setErr(String(e));
}
});
document.getElementById("btnState").addEventListener("click", async () => {
setErr("");
await refreshState();
});
async function refreshState() {
try {
const r = await fetch("/web/state");
const t = await r.text();
let st;
try {
st = JSON.parse(t);
} catch (e) {
setErr("Get state: bad JSON");
return;
}
if (r.ok) {
updateStateFromServer(st);
}
} catch (e) {
setErr(String(e));
}
}
</script>
</body>
</html>