sentinel-env / static /index.html
XcodeAddy's picture
Add process-aware reward engine reports
b3b9bbd
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>SENTINEL Trust Mission Control</title>
<style>
:root {
--bg: #060706;
--panel: #10130f;
--panel-2: #151911;
--panel-3: #1b2017;
--ink: #f6f1df;
--muted: #aaa48e;
--faint: #6e705f;
--line: #343a2d;
--jade: #27e0a1;
--jade-soft: rgba(39, 224, 161, 0.14);
--amber: #f5ba41;
--amber-soft: rgba(245, 186, 65, 0.14);
--flame: #ff5f45;
--flame-soft: rgba(255, 95, 69, 0.14);
--blue: #73a7ff;
--blue-soft: rgba(115, 167, 255, 0.14);
--magenta: #e879f9;
--magenta-soft: rgba(232, 121, 249, 0.12);
--cream: #fff6d8;
--shadow: 0 22px 70px rgba(0, 0, 0, 0.38);
}
* {
box-sizing: border-box;
}
html {
background: var(--bg);
}
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
linear-gradient(rgba(255, 255, 255, 0.025) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.022) 1px, transparent 1px),
linear-gradient(145deg, #060706 0%, #11130d 44%, #0b0e0a 100%);
background-size: 32px 32px, 32px 32px, auto;
font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
letter-spacing: 0;
}
button,
select,
input {
font: inherit;
}
button {
min-height: 40px;
border: 1px solid #4a5241;
background: #171b13;
color: var(--ink);
border-radius: 8px;
padding: 0 13px;
cursor: pointer;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
white-space: nowrap;
transition: transform 160ms ease, border-color 160ms ease, background 160ms ease;
}
button:hover {
border-color: #80906f;
background: #202719;
transform: translateY(-1px);
}
button:disabled {
cursor: not-allowed;
opacity: 0.52;
transform: none;
}
button.primary {
border-color: rgba(39, 224, 161, 0.72);
background: linear-gradient(180deg, #1e6b54, #134736);
color: #f7fffb;
}
button.warn {
border-color: rgba(245, 186, 65, 0.72);
background: linear-gradient(180deg, #73551d, #403012);
color: #fff4d0;
}
button.danger {
border-color: rgba(255, 95, 69, 0.72);
background: #2a1510;
color: #ffd8d1;
}
select,
input {
min-height: 40px;
border: 1px solid #4a5241;
background: #0f120d;
color: var(--ink);
border-radius: 8px;
padding: 0 11px;
outline: none;
}
select:focus,
input:focus,
button:focus {
border-color: var(--jade);
box-shadow: 0 0 0 3px rgba(39, 224, 161, 0.16);
}
input {
width: 90px;
}
.shell {
min-height: 100vh;
}
header {
position: sticky;
top: 0;
z-index: 20;
display: grid;
grid-template-columns: minmax(280px, 1fr) auto;
gap: 18px;
align-items: center;
padding: 16px 22px;
background: rgba(7, 8, 6, 0.94);
border-bottom: 1px solid #2e3529;
backdrop-filter: blur(14px);
}
.brand {
display: flex;
align-items: center;
gap: 14px;
min-width: 0;
}
.mark {
width: 42px;
height: 42px;
border: 1px solid rgba(39, 224, 161, 0.64);
border-radius: 8px;
display: grid;
place-items: center;
background:
linear-gradient(135deg, rgba(39, 224, 161, 0.28), rgba(245, 186, 65, 0.1)),
#10150f;
color: #dfffee;
font-weight: 860;
box-shadow: inset 0 0 22px rgba(39, 224, 161, 0.16);
}
h1,
h2,
h3,
p {
margin: 0;
}
h1 {
font-size: 20px;
line-height: 1.05;
font-weight: 840;
}
.subhead {
margin-top: 5px;
color: var(--muted);
font-size: 13px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.header-actions {
display: flex;
align-items: center;
justify-content: flex-end;
gap: 9px;
flex-wrap: wrap;
}
.modebar {
padding: 0 22px 14px;
border-bottom: 1px solid #232920;
background: rgba(7, 8, 6, 0.9);
backdrop-filter: blur(14px);
}
.modebar-inner {
width: min(1540px, 100%);
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 14px;
flex-wrap: wrap;
}
.view-tabs {
display: flex;
align-items: center;
gap: 8px;
flex-wrap: wrap;
}
.view-tab {
min-height: 38px;
border-radius: 999px;
padding: 0 14px;
background: #10140f;
}
.view-tab.active {
border-color: rgba(39, 224, 161, 0.72);
background: linear-gradient(180deg, #1d6a53, #133c30);
color: #effff7;
}
.view-copy {
color: var(--muted);
font-size: 13px;
line-height: 1.4;
max-width: 760px;
}
.console {
width: min(1540px, 100%);
margin: 0 auto;
padding: 18px;
display: grid;
gap: 14px;
grid-template-columns: minmax(420px, 1.35fr) minmax(340px, 0.85fr);
grid-template-areas:
"hero hero"
"theater command"
"mission playground"
"trust playground"
"story judge"
"proof events"
"flow themes"
"readiness readiness";
align-items: start;
}
section {
min-width: 0;
background: linear-gradient(180deg, rgba(255, 255, 255, 0.035), transparent 38%), var(--panel);
border: 1px solid var(--line);
border-radius: 8px;
box-shadow: var(--shadow);
overflow: hidden;
}
.hero { grid-area: hero; }
.theater { grid-area: theater; }
.command { grid-area: command; }
.mission { grid-area: mission; }
.trust { grid-area: trust; }
.playground { grid-area: playground; }
.story { grid-area: story; }
.judge { grid-area: judge; }
.readiness { grid-area: readiness; }
.proof { grid-area: proof; }
.events { grid-area: events; }
.flow { grid-area: flow; }
.themes { grid-area: themes; }
.section-hidden {
display: none;
}
.section-head {
min-height: 54px;
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 13px 15px;
border-bottom: 1px solid var(--line);
background: rgba(0, 0, 0, 0.14);
}
h2 {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0;
color: #d6d0b7;
font-weight: 820;
}
.body {
padding: 15px;
}
.hero-grid {
display: grid;
grid-template-columns: minmax(0, 1.1fr) minmax(320px, 0.9fr);
gap: 13px;
}
.hero-panel {
min-height: 208px;
border: 1px solid #394132;
border-radius: 8px;
padding: 15px;
background: var(--panel-2);
}
.hero-panel.primary {
border-color: rgba(39, 224, 161, 0.42);
background:
linear-gradient(180deg, rgba(39, 224, 161, 0.12), transparent 45%),
var(--panel-2);
}
.hero-panel h3,
.judge-card h3 {
font-size: 18px;
color: var(--cream);
margin: 0 0 10px 0;
}
.hero-panel p {
color: #e8e1ca;
font-size: 14px;
line-height: 1.55;
}
.hero-callouts,
.hero-steps,
.judge-list {
display: grid;
gap: 9px;
margin-top: 14px;
}
.hero-callout,
.hero-step,
.judge-step {
min-height: 52px;
border: 1px solid #394132;
border-radius: 8px;
padding: 11px 12px;
background: #0d100b;
color: #ebe5cf;
line-height: 1.42;
}
.hero-callout strong,
.hero-step strong,
.judge-step strong {
display: block;
margin-bottom: 4px;
color: var(--cream);
font-size: 13px;
}
.hero-stats {
margin-top: 14px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.hero-stat {
min-height: 80px;
border: 1px solid #394132;
border-radius: 8px;
padding: 11px;
background: #0d100b;
}
.hero-stat .label {
margin-bottom: 7px;
}
.hero-stat .value {
font-size: 22px;
}
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
align-items: center;
justify-content: flex-end;
}
.chip {
display: inline-flex;
align-items: center;
justify-content: center;
min-height: 27px;
border: 1px solid #46503d;
border-radius: 999px;
padding: 0 9px;
font-size: 12px;
font-weight: 760;
color: #d8d1ba;
background: #161a13;
font-variant-numeric: tabular-nums;
}
.chip.live {
border-color: rgba(39, 224, 161, 0.45);
color: #a7f9d8;
background: var(--jade-soft);
}
.chip.warn {
border-color: rgba(245, 186, 65, 0.46);
color: #ffe0a3;
background: var(--amber-soft);
}
.chip.fail {
border-color: rgba(255, 95, 69, 0.52);
color: #ffc3b8;
background: var(--flame-soft);
}
.muted {
color: var(--muted);
}
.stage {
position: relative;
min-height: 464px;
border: 1px solid #333c2b;
border-radius: 8px;
background:
linear-gradient(90deg, transparent 0, transparent calc(50% - 1px), rgba(245, 186, 65, 0.28) 50%, transparent calc(50% + 1px), transparent 100%),
linear-gradient(rgba(39, 224, 161, 0.055) 1px, transparent 1px),
linear-gradient(90deg, rgba(39, 224, 161, 0.04) 1px, transparent 1px),
#080a08;
background-size: auto, 26px 26px, 26px 26px, auto;
overflow: hidden;
}
.stage::after {
content: "";
position: absolute;
inset: 0;
pointer-events: none;
border: 1px solid rgba(255, 255, 255, 0.045);
border-radius: inherit;
}
.stage-topline {
position: absolute;
top: 13px;
left: 14px;
right: 14px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
z-index: 2;
}
.stage-label {
min-height: 46px;
border: 1px solid #343c2e;
border-radius: 8px;
padding: 8px 10px;
background: rgba(13, 16, 11, 0.86);
}
.stage-label span {
display: block;
color: var(--muted);
font-size: 11px;
line-height: 1.2;
}
.stage-label strong {
display: block;
margin-top: 4px;
font-size: 15px;
color: var(--cream);
overflow-wrap: anywhere;
}
.brain-card {
position: absolute;
top: 95px;
left: 50%;
transform: translateX(-50%);
width: min(360px, calc(100% - 32px));
min-height: 96px;
border: 1px solid rgba(39, 224, 161, 0.56);
border-radius: 8px;
padding: 15px;
background:
linear-gradient(180deg, rgba(39, 224, 161, 0.18), rgba(39, 224, 161, 0.035)),
#0f1711;
box-shadow: 0 0 0 1px rgba(39, 224, 161, 0.06), 0 18px 42px rgba(0, 0, 0, 0.42);
text-align: center;
z-index: 3;
}
.node-title {
font-size: 18px;
font-weight: 820;
color: #ecfff5;
}
.node-copy {
margin-top: 6px;
color: #b8d7c5;
font-size: 13px;
line-height: 1.35;
}
.signal-line {
position: absolute;
left: 50%;
top: 190px;
bottom: 178px;
width: 1px;
background: linear-gradient(180deg, rgba(39, 224, 161, 0.7), rgba(245, 186, 65, 0.6), rgba(255, 95, 69, 0.55));
opacity: 0.9;
z-index: 1;
}
.specialist-grid {
position: absolute;
left: 15px;
right: 15px;
bottom: 96px;
display: grid;
grid-template-columns: repeat(5, minmax(92px, 1fr));
gap: 10px;
z-index: 3;
}
.node {
min-height: 116px;
border: 1px solid #3a4433;
border-radius: 8px;
padding: 10px;
background: rgba(16, 19, 15, 0.94);
position: relative;
overflow: hidden;
}
.node::before {
content: "";
position: absolute;
left: 0;
right: 0;
top: 0;
height: 3px;
background: var(--node-color, var(--blue));
}
.node.active {
border-color: var(--jade);
box-shadow: 0 0 0 2px rgba(39, 224, 161, 0.13);
}
.node.watch {
border-color: rgba(245, 186, 65, 0.72);
}
.node.quarantine {
border-color: rgba(255, 95, 69, 0.74);
}
.node-id {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
color: #f9f3d9;
font-weight: 820;
font-size: 16px;
}
.node-trust {
margin-top: 14px;
display: grid;
gap: 8px;
}
.trust-rail {
height: 10px;
border-radius: 999px;
background: #2a3024;
overflow: hidden;
}
.trust-rail span {
display: block;
height: 100%;
width: calc(var(--trust, 0.5) * 100%);
border-radius: inherit;
background: var(--node-color, var(--blue));
transition: width 220ms ease, background 220ms ease;
}
.node-meta {
display: flex;
justify-content: space-between;
gap: 8px;
color: var(--muted);
font-size: 12px;
font-variant-numeric: tabular-nums;
}
.outcome-strip {
position: absolute;
left: 15px;
right: 15px;
bottom: 14px;
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
z-index: 3;
}
.outcome {
min-height: 62px;
border: 1px solid #373f31;
border-radius: 8px;
padding: 10px;
background: rgba(11, 13, 9, 0.91);
}
.outcome span {
color: var(--muted);
font-size: 11px;
display: block;
}
.outcome strong {
display: block;
margin-top: 5px;
font-size: 15px;
color: var(--cream);
overflow-wrap: anywhere;
}
.command-grid {
display: grid;
gap: 12px;
}
.field-row {
display: grid;
grid-template-columns: 1fr 96px;
gap: 10px;
}
.button-row {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.action-row {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 9px;
}
.decision-card {
border: 1px solid #394132;
border-radius: 8px;
padding: 13px;
background: var(--panel-2);
}
.decision-title {
display: flex;
justify-content: space-between;
align-items: center;
gap: 10px;
margin-bottom: 10px;
}
.decision-title strong {
color: var(--cream);
}
.recommendation {
display: grid;
grid-template-columns: 1fr auto;
gap: 10px;
align-items: center;
margin-top: 10px;
}
.recommendation-output {
min-height: 44px;
border: 1px solid rgba(39, 224, 161, 0.32);
border-radius: 8px;
padding: 9px 10px;
background: rgba(39, 224, 161, 0.08);
color: #c9ffe8;
font-weight: 820;
overflow-wrap: anywhere;
}
.stats-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.stat {
min-height: 82px;
border: 1px solid #394132;
border-radius: 8px;
padding: 11px;
background: var(--panel-2);
}
.label {
color: var(--muted);
font-size: 12px;
line-height: 1.2;
margin-bottom: 8px;
}
.value {
font-size: 24px;
line-height: 1;
color: var(--cream);
font-weight: 840;
font-variant-numeric: tabular-nums;
overflow-wrap: anywhere;
}
.subtask {
margin-top: 12px;
min-height: 84px;
border: 1px solid #3a4433;
border-radius: 8px;
padding: 13px;
background: #0d100b;
color: #e8e1c9;
line-height: 1.45;
}
.progress {
height: 12px;
margin-top: 12px;
border-radius: 999px;
overflow: hidden;
background: #2a3024;
}
.progress span {
display: block;
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--flame), var(--amber), var(--jade));
border-radius: inherit;
transition: width 220ms ease;
}
.risk-meter {
display: grid;
gap: 8px;
margin-top: 12px;
}
.risk-track {
height: 12px;
border-radius: 999px;
background: #2a3024;
overflow: hidden;
}
.risk-track span {
display: block;
height: 100%;
width: 0%;
background: linear-gradient(90deg, var(--jade), var(--amber), var(--flame));
border-radius: inherit;
transition: width 220ms ease;
}
.trust-list {
display: grid;
gap: 9px;
}
.specialist {
display: grid;
grid-template-columns: 50px 1fr 58px;
gap: 11px;
align-items: center;
min-height: 52px;
border: 1px solid #394132;
border-radius: 8px;
padding: 10px;
background: var(--panel-2);
}
.sid {
color: var(--cream);
font-weight: 820;
}
.bar {
height: 12px;
border-radius: 999px;
background: #2a3024;
overflow: hidden;
}
.fill {
height: 100%;
width: 50%;
border-radius: inherit;
transition: width 220ms ease, background 220ms ease;
}
.score {
text-align: right;
color: #e8e1c9;
font-weight: 820;
font-variant-numeric: tabular-nums;
}
.proof-grid {
display: grid;
grid-template-columns: minmax(280px, 0.95fr) minmax(300px, 1.05fr);
gap: 13px;
align-items: stretch;
}
.baseline-table {
display: grid;
gap: 10px;
}
.baseline-row {
display: grid;
grid-template-columns: 98px 1fr 56px;
align-items: center;
gap: 10px;
min-height: 39px;
color: #e9e2ca;
font-size: 13px;
}
.mini-bar {
height: 12px;
border-radius: 999px;
background: #2a3024;
overflow: hidden;
}
.mini-bar span {
display: block;
height: 100%;
border-radius: inherit;
}
.chart-frame {
min-height: 232px;
border: 1px solid #394132;
border-radius: 8px;
background: #f7f4ec;
padding: 8px;
display: grid;
place-items: center;
}
.chart-frame img {
display: block;
width: 100%;
max-height: 360px;
object-fit: contain;
border-radius: 4px;
}
.json-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.json-panel {
min-height: 248px;
border: 1px solid #394132;
border-radius: 8px;
background: #0c100a;
overflow: hidden;
}
.json-head {
min-height: 44px;
padding: 10px 12px;
border-bottom: 1px solid #394132;
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
color: var(--cream);
font-size: 13px;
font-weight: 780;
background: rgba(255, 255, 255, 0.02);
}
.json-head span {
color: var(--muted);
font-weight: 620;
font-size: 12px;
}
.json-block {
margin: 0;
min-height: 204px;
padding: 12px;
overflow: auto;
color: #d7fbe8;
font-size: 12px;
line-height: 1.48;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
white-space: pre-wrap;
word-break: break-word;
}
.playground-meta {
margin-top: 12px;
display: grid;
grid-template-columns: 168px 1fr;
gap: 12px;
}
.playground-card {
min-height: 96px;
border: 1px solid #394132;
border-radius: 8px;
padding: 12px;
background: var(--panel-2);
}
.playground-card strong {
display: block;
color: var(--cream);
margin-bottom: 7px;
font-size: 14px;
}
.playground-card span {
display: block;
color: var(--muted);
font-size: 13px;
line-height: 1.4;
}
.story-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 12px;
}
.story-lane {
min-height: 250px;
border: 1px solid #394132;
border-radius: 8px;
padding: 14px;
background: var(--panel-2);
}
.story-lane.before {
border-color: rgba(255, 95, 69, 0.42);
background: var(--flame-soft);
}
.story-lane.after {
border-color: rgba(39, 224, 161, 0.42);
background: var(--jade-soft);
}
.story-title {
display: flex;
align-items: center;
justify-content: space-between;
gap: 10px;
margin-bottom: 12px;
}
.story-title strong {
color: var(--cream);
font-size: 16px;
}
.story-score {
border-radius: 999px;
padding: 4px 9px;
font-size: 12px;
font-weight: 820;
color: #0b0d08;
background: var(--cream);
font-variant-numeric: tabular-nums;
}
.story-flow {
display: grid;
gap: 9px;
}
.story-step {
min-height: 48px;
border: 1px solid #394132;
border-radius: 8px;
padding: 10px;
background: rgba(10, 12, 8, 0.68);
color: #ece6cf;
font-size: 13px;
line-height: 1.35;
}
.story-note {
margin-top: 12px;
min-height: 58px;
border: 1px dashed #4a5241;
border-radius: 8px;
padding: 11px;
color: var(--muted);
font-size: 13px;
line-height: 1.4;
background: rgba(10, 12, 8, 0.35);
}
.judge-grid {
display: grid;
gap: 12px;
}
.judge-stats {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.judge-card {
min-height: 132px;
border: 1px solid #394132;
border-radius: 8px;
padding: 13px;
background: var(--panel-2);
}
.judge-card.good {
border-color: rgba(39, 224, 161, 0.4);
background: var(--jade-soft);
}
.judge-card.warn {
border-color: rgba(245, 186, 65, 0.4);
background: var(--amber-soft);
}
.judge-card.bad {
border-color: rgba(255, 95, 69, 0.4);
background: var(--flame-soft);
}
.judge-card .value {
font-size: 28px;
margin-top: 8px;
}
.judge-card .muted {
display: block;
margin-top: 6px;
line-height: 1.4;
}
.judge-actions {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 10px;
}
.readiness-list {
display: grid;
gap: 10px;
}
.readiness-item {
min-height: 64px;
border: 1px solid rgba(39, 224, 161, 0.38);
border-radius: 8px;
padding: 11px 12px;
background: rgba(39, 224, 161, 0.08);
}
.readiness-item.pending {
border-color: rgba(245, 186, 65, 0.38);
background: rgba(245, 186, 65, 0.09);
}
.readiness-item strong {
display: block;
color: var(--cream);
margin-bottom: 5px;
font-size: 14px;
}
.readiness-item span {
display: block;
color: var(--muted);
font-size: 13px;
line-height: 1.35;
}
.event-list {
display: grid;
gap: 8px;
max-height: 432px;
overflow: auto;
padding-right: 2px;
}
.event {
display: grid;
grid-template-columns: 48px 1fr 58px;
gap: 10px;
align-items: start;
min-height: 52px;
border: 1px solid #394132;
border-radius: 8px;
padding: 9px;
background: var(--panel-2);
font-size: 13px;
}
.event strong {
color: var(--cream);
}
.event .reward {
text-align: right;
color: var(--jade);
font-weight: 840;
font-variant-numeric: tabular-nums;
}
.flow-line {
display: grid;
grid-template-columns: repeat(6, minmax(0, 1fr));
gap: 9px;
}
.flow-step {
position: relative;
min-height: 112px;
border: 1px solid #394132;
border-radius: 8px;
padding: 12px;
background: var(--panel-2);
}
.flow-step::before {
content: attr(data-num);
display: grid;
place-items: center;
width: 28px;
height: 28px;
border-radius: 50%;
border: 1px solid rgba(39, 224, 161, 0.45);
color: #b7ffdf;
background: #101a13;
font-weight: 820;
margin-bottom: 10px;
}
.flow-step strong {
display: block;
color: var(--cream);
margin-bottom: 7px;
}
.flow-step span {
display: block;
color: var(--muted);
font-size: 12px;
line-height: 1.35;
}
.theme-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 10px;
}
.theme-card {
min-height: 132px;
border: 1px solid #394132;
border-radius: 8px;
padding: 12px;
background: var(--panel-2);
}
.theme-card strong {
display: block;
color: var(--cream);
margin-bottom: 8px;
font-size: 15px;
}
.theme-card span {
display: block;
color: var(--muted);
line-height: 1.38;
font-size: 13px;
}
.theme-card.blue {
border-color: rgba(115, 167, 255, 0.4);
background: var(--blue-soft);
}
.theme-card.green {
border-color: rgba(39, 224, 161, 0.4);
background: var(--jade-soft);
}
.theme-card.amber {
border-color: rgba(245, 186, 65, 0.4);
background: var(--amber-soft);
}
.theme-card.magenta {
border-color: rgba(232, 121, 249, 0.4);
background: var(--magenta-soft);
}
/* Phase 4 visual system rebuild: calmer glass surfaces, cleaner hierarchy,
and per-mode layouts so the page feels like a product instead of one giant board. */
:root {
--bg: #060912;
--panel: rgba(15, 19, 30, 0.72);
--panel-2: rgba(20, 25, 39, 0.68);
--panel-3: rgba(26, 31, 47, 0.66);
--ink: #f4f7ff;
--muted: #a4aec4;
--faint: #71809f;
--line: rgba(255, 255, 255, 0.1);
--jade: #63dfc0;
--jade-soft: rgba(99, 223, 192, 0.14);
--amber: #ffbf7d;
--amber-soft: rgba(255, 191, 125, 0.14);
--flame: #ff8d93;
--flame-soft: rgba(255, 141, 147, 0.14);
--blue: #8eb5ff;
--blue-soft: rgba(142, 181, 255, 0.14);
--magenta: #c5a3ff;
--magenta-soft: rgba(197, 163, 255, 0.14);
--cream: #f7f9ff;
--shadow: 0 24px 60px rgba(3, 8, 20, 0.36);
}
html {
background: var(--bg);
}
body {
background:
radial-gradient(circle at 12% 0%, rgba(142, 181, 255, 0.18), transparent 24%),
radial-gradient(circle at 88% 8%, rgba(197, 163, 255, 0.14), transparent 22%),
radial-gradient(circle at 60% 100%, rgba(99, 223, 192, 0.08), transparent 22%),
linear-gradient(180deg, #05070d 0%, #0a0d15 46%, #070a12 100%);
background-attachment: fixed;
color: var(--ink);
}
body::before {
content: "";
position: fixed;
inset: 0;
pointer-events: none;
background:
linear-gradient(rgba(255, 255, 255, 0.022) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.018) 1px, transparent 1px);
background-size: 42px 42px;
mask-image: radial-gradient(circle at center, black 35%, transparent 82%);
opacity: 0.34;
}
header,
.modebar {
background: rgba(8, 11, 19, 0.72);
border-color: rgba(255, 255, 255, 0.08);
backdrop-filter: blur(24px) saturate(1.3);
}
.brand {
gap: 16px;
}
.mark {
border-color: rgba(255, 255, 255, 0.12);
background:
radial-gradient(circle at 30% 28%, rgba(142, 181, 255, 0.45), transparent 58%),
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)),
rgba(17, 22, 34, 0.82);
color: #eef4ff;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.18),
0 12px 34px rgba(5, 10, 22, 0.34);
}
h1 {
font-size: 22px;
color: #f7faff;
}
.subhead,
.view-copy,
.muted,
.label,
.node-copy,
.playground-card span,
.theme-card span,
.readiness-item span,
.flow-step span {
color: var(--muted);
}
button,
select,
input,
.view-tab {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.045);
color: var(--ink);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
button:hover,
.view-tab:hover {
border-color: rgba(255, 255, 255, 0.16);
background: rgba(255, 255, 255, 0.08);
}
button.primary,
.view-tab.active {
border-color: rgba(142, 181, 255, 0.38);
background:
linear-gradient(180deg, rgba(142, 181, 255, 0.22), rgba(142, 181, 255, 0.1)),
rgba(255, 255, 255, 0.05);
color: #f3f8ff;
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.12);
}
button.warn {
border-color: rgba(255, 191, 125, 0.32);
background:
linear-gradient(180deg, rgba(255, 191, 125, 0.2), rgba(255, 191, 125, 0.08)),
rgba(255, 255, 255, 0.04);
color: #fff1e0;
}
button.danger {
border-color: rgba(255, 141, 147, 0.3);
background:
linear-gradient(180deg, rgba(255, 141, 147, 0.2), rgba(255, 141, 147, 0.08)),
rgba(255, 255, 255, 0.04);
color: #ffe9eb;
}
.console {
width: min(1480px, 100%);
gap: 16px;
}
.console[data-view="overview"] {
grid-template-columns: minmax(420px, 1.02fr) minmax(360px, 0.98fr);
grid-template-areas:
"hero hero"
"story proof"
"flow themes"
"readiness readiness";
}
.console[data-view="playground"] {
grid-template-columns: minmax(520px, 1.24fr) minmax(360px, 0.76fr);
grid-template-areas:
"theater command"
"mission playground"
"trust playground"
"events playground";
}
.console[data-view="judge"] {
grid-template-columns: minmax(500px, 1.05fr) minmax(360px, 0.95fr);
grid-template-areas:
"hero hero"
"story proof"
"theater judge"
"mission judge";
}
section,
.hero-panel,
.decision-card,
.stat,
.playground-card,
.json-panel,
.story-lane,
.judge-card,
.readiness-item,
.event,
.flow-step,
.theme-card,
.node,
.outcome,
.stage-label {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)),
var(--panel);
border-color: rgba(255, 255, 255, 0.1);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 18px 50px rgba(3, 8, 20, 0.22);
backdrop-filter: blur(22px) saturate(1.3);
}
.section-head,
.json-head {
background: rgba(255, 255, 255, 0.025);
border-bottom-color: rgba(255, 255, 255, 0.07);
}
h2 {
color: rgba(247, 249, 255, 0.84);
letter-spacing: 0.08em;
}
.chip {
border-color: rgba(255, 255, 255, 0.1);
background: rgba(255, 255, 255, 0.055);
color: #dde5f6;
}
.chip.live {
border-color: rgba(142, 181, 255, 0.24);
color: #dbe8ff;
background: rgba(142, 181, 255, 0.12);
}
.chip.warn {
border-color: rgba(255, 191, 125, 0.22);
color: #ffe7c8;
background: rgba(255, 191, 125, 0.12);
}
.chip.fail {
border-color: rgba(255, 141, 147, 0.22);
color: #ffd7da;
background: rgba(255, 141, 147, 0.12);
}
.stage {
border-color: rgba(255, 255, 255, 0.11);
background:
radial-gradient(circle at 50% 12%, rgba(142, 181, 255, 0.22), transparent 18%),
radial-gradient(circle at 82% 82%, rgba(197, 163, 255, 0.18), transparent 18%),
linear-gradient(rgba(255, 255, 255, 0.018) 1px, transparent 1px),
linear-gradient(90deg, rgba(255, 255, 255, 0.016) 1px, transparent 1px),
linear-gradient(180deg, rgba(255, 255, 255, 0.04), rgba(255, 255, 255, 0.01)),
rgba(9, 13, 23, 0.72);
background-size: auto, auto, 32px 32px, 32px 32px, auto, auto;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.05),
0 24px 60px rgba(3, 8, 20, 0.28);
}
.stage::after {
border-color: rgba(255, 255, 255, 0.06);
}
.brain-card {
border-color: rgba(142, 181, 255, 0.28);
background:
radial-gradient(circle at 20% 20%, rgba(142, 181, 255, 0.22), transparent 42%),
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)),
rgba(18, 23, 35, 0.72);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.08),
0 22px 48px rgba(3, 8, 20, 0.32);
}
.signal-line {
background: linear-gradient(180deg, rgba(142, 181, 255, 0.72), rgba(197, 163, 255, 0.56), rgba(99, 223, 192, 0.46));
}
.node.active {
border-color: rgba(142, 181, 255, 0.34);
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.06),
0 0 0 1px rgba(142, 181, 255, 0.16);
}
.node.watch {
border-color: rgba(255, 191, 125, 0.34);
}
.node.quarantine {
border-color: rgba(255, 141, 147, 0.38);
}
.node-id,
.node-title,
.outcome strong,
.hero-panel h3,
.judge-card h3,
.story-title strong,
.decision-title strong,
.theme-card strong,
.flow-step strong,
.readiness-item strong,
.playground-card strong {
color: #f7f9ff;
}
.trust-rail,
.bar,
.progress,
.risk-track,
.mini-bar {
background: rgba(255, 255, 255, 0.08);
}
.progress span {
background: linear-gradient(90deg, #ff9e8c, #c5a3ff, #8eb5ff);
}
.risk-track span {
background: linear-gradient(90deg, #63dfc0, #8eb5ff, #ffbf7d);
}
.recommendation-output {
border-color: rgba(142, 181, 255, 0.2);
background: rgba(142, 181, 255, 0.1);
color: #e6efff;
}
.story-lane.before,
.judge-card.bad {
background:
linear-gradient(180deg, rgba(255, 141, 147, 0.14), rgba(255, 141, 147, 0.05)),
rgba(18, 22, 34, 0.7);
border-color: rgba(255, 141, 147, 0.2);
}
.story-lane.after,
.judge-card.good {
background:
linear-gradient(180deg, rgba(142, 181, 255, 0.14), rgba(99, 223, 192, 0.06)),
rgba(18, 22, 34, 0.7);
border-color: rgba(142, 181, 255, 0.2);
}
.judge-card.warn,
.readiness-item.pending {
background:
linear-gradient(180deg, rgba(255, 191, 125, 0.14), rgba(255, 191, 125, 0.05)),
rgba(18, 22, 34, 0.7);
border-color: rgba(255, 191, 125, 0.2);
}
.story-score {
color: #f4f7ff;
background: rgba(255, 255, 255, 0.08);
border: 1px solid rgba(255, 255, 255, 0.1);
}
.chart-frame {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.12), rgba(255, 255, 255, 0.04)),
rgba(246, 248, 253, 0.96);
border-color: rgba(255, 255, 255, 0.2);
}
.hero-panel.primary {
border-color: rgba(142, 181, 255, 0.18);
background:
radial-gradient(circle at 82% 12%, rgba(99, 223, 192, 0.12), transparent 22%),
radial-gradient(circle at 18% 0%, rgba(142, 181, 255, 0.18), transparent 24%),
linear-gradient(180deg, rgba(255, 255, 255, 0.08), rgba(255, 255, 255, 0.02)),
rgba(18, 22, 34, 0.72);
}
.hero-callout,
.hero-step,
.judge-step,
.story-step,
.story-note,
.hero-stat {
background:
linear-gradient(180deg, rgba(255, 255, 255, 0.055), rgba(255, 255, 255, 0.015)),
rgba(11, 15, 24, 0.56);
border-color: rgba(255, 255, 255, 0.08);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04);
}
.hero-stat .value,
.judge-card .value,
.value {
color: #f7f9ff;
}
.json-block {
color: #dce8ff;
}
.event .reward {
color: #9bd0ff;
}
::-webkit-scrollbar {
width: 10px;
height: 10px;
}
::-webkit-scrollbar-thumb {
background: rgba(255, 255, 255, 0.14);
border-radius: 999px;
}
::-webkit-scrollbar-track {
background: rgba(255, 255, 255, 0.04);
}
@media (max-width: 1180px) {
.console {
grid-template-columns: 1fr;
grid-template-areas:
"hero"
"theater"
"command"
"mission"
"trust"
"playground"
"story"
"judge"
"readiness"
"proof"
"events"
"flow"
"themes";
}
.console[data-view="overview"] {
grid-template-columns: 1fr;
grid-template-areas:
"hero"
"story"
"proof"
"flow"
"themes"
"readiness";
}
.console[data-view="playground"] {
grid-template-columns: 1fr;
grid-template-areas:
"theater"
"command"
"mission"
"trust"
"playground"
"events";
}
.console[data-view="judge"] {
grid-template-columns: 1fr;
grid-template-areas:
"hero"
"story"
"proof"
"theater"
"mission"
"judge";
}
header {
grid-template-columns: 1fr;
}
.header-actions {
justify-content: flex-start;
}
}
@media (max-width: 860px) {
.console {
padding: 12px;
}
.stage {
min-height: 620px;
}
.stage-topline,
.outcome-strip,
.hero-grid,
.hero-stats,
.proof-grid,
.json-grid,
.playground-meta,
.story-grid,
.judge-stats,
.judge-actions,
.flow-line,
.theme-grid,
.stats-grid {
grid-template-columns: 1fr;
}
.brain-card {
top: 190px;
}
.signal-line {
display: none;
}
.specialist-grid {
top: 315px;
bottom: auto;
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.outcome-strip {
bottom: 12px;
}
.action-row {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
}
@media (max-width: 560px) {
header {
padding: 13px;
}
.field-row,
.button-row,
.recommendation,
.specialist,
.event,
.baseline-row {
grid-template-columns: 1fr;
}
input {
width: 100%;
}
.modebar {
padding: 0 13px 12px;
}
.view-tabs {
width: 100%;
flex-direction: column;
align-items: stretch;
}
.view-tab {
width: 100%;
}
.specialist-grid {
grid-template-columns: 1fr;
}
.stage {
min-height: 980px;
}
.event .reward,
.score {
text-align: left;
}
}
</style>
</head>
<body>
<div class="shell">
<header>
<div class="brand">
<div class="mark">S</div>
<div>
<h1>SENTINEL Trust Mission Control</h1>
<div class="subhead">OpenEnv RL environment for adversarial multi-agent trust calibration</div>
</div>
</div>
<div class="header-actions">
<select id="taskSelect" aria-label="Task">
<option value="task1">Task 1 - Easy</option>
<option value="task2">Task 2 - Medium</option>
<option value="task3" selected>Task 3 - Hard</option>
</select>
<input id="seedInput" aria-label="Seed" type="number" value="42">
<button id="resetBtn" class="primary" type="button">Reset Episode</button>
<button id="swapBtn" class="warn" type="button">Swap Profiles</button>
<button id="autoBtn" type="button">Heuristic Auto</button>
</div>
</header>
<div class="modebar">
<div class="modebar-inner">
<div class="view-tabs">
<button id="viewOverviewBtn" class="view-tab active" type="button">Overview</button>
<button id="viewPlaygroundBtn" class="view-tab" type="button">Playground</button>
<button id="viewJudgeBtn" class="view-tab" type="button">Judge Demo</button>
</div>
<div id="viewCopy" class="view-copy">Overview turns the environment into a judge-readable system story: the problem, the learning signal, and the live failure mode it fixes.</div>
</div>
</div>
<main id="console" class="console">
<section class="hero">
<div class="section-head">
<h2>System Overview</h2>
<div class="chips">
<span class="chip live">reset → step → state</span>
<span class="chip">OpenEnv compatible</span>
<span class="chip warn">skill, not identity</span>
</div>
</div>
<div class="body">
<div class="hero-grid">
<div class="hero-panel primary">
<h3>What SENTINEL actually teaches</h3>
<p>SENTINEL is not training a specialist to solve one domain task. It trains the orchestrator to decide who to trust, when to verify, when to self-solve, and how to recover when one public slot turns unreliable or adversarial inside a long multi-agent task graph.</p>
<div class="hero-callouts">
<div class="hero-callout">
<strong>Observation model</strong>
The orchestrator only sees behavior: public slots, trust scores, stakes, step budget, and outcomes.
</div>
<div class="hero-callout">
<strong>Core novelty</strong>
Hidden specialist profiles reshuffle every reset, so the agent cannot memorize that S2 or S3 is dangerous.
</div>
<div class="hero-callout">
<strong>Judge takeaway</strong>
This environment turns blind agent-to-agent trust into a trainable oversight skill.
</div>
</div>
<div class="hero-stats">
<div class="hero-stat">
<div class="label">Random overall</div>
<div id="heroRandomScore" class="value">0.695</div>
</div>
<div class="hero-stat">
<div class="label">Heuristic overall</div>
<div id="heroHeuristicScore" class="value">0.796</div>
</div>
<div class="hero-stat">
<div class="label">Task 3 detect</div>
<div id="heroDetectionScore" class="value">0.735</div>
</div>
</div>
</div>
<div class="hero-panel">
<h3>How to test this fast</h3>
<div class="hero-steps">
<div class="hero-step">
<strong>1. Overview mode</strong>
Read the before/after lanes and reward proof. This tells the story in judge language.
</div>
<div class="hero-step">
<strong>2. Playground mode</strong>
Reset an episode, click Auto Policy, and watch the API payloads, trust bars, and reward stream update.
</div>
<div class="hero-step">
<strong>3. Judge Demo mode</strong>
Run Random, then Heuristic, then Swap + Replay. That is the live finale sequence.
</div>
</div>
</div>
</div>
</div>
</section>
<section class="theater">
<div class="section-head">
<h2>Live Trust Theater</h2>
<div class="chips">
<span id="statusChip" class="chip live">READY</span>
<span id="scenarioChip" class="chip">SCENARIO</span>
</div>
</div>
<div class="body">
<div class="stage">
<div class="stage-topline">
<div class="stage-label">
<span>Profile rule</span>
<strong>reshuffle on reset</strong>
</div>
<div class="stage-label">
<span>Observation rule</span>
<strong>behavior only</strong>
</div>
<div class="stage-label">
<span>Failure mode</span>
<strong>high-stakes poison</strong>
</div>
</div>
<div class="brain-card">
<div class="node-title">Orchestrator</div>
<div id="leadMove" class="node-copy">Reset starts a fresh trust game.</div>
</div>
<div class="signal-line"></div>
<div class="specialist-grid" id="networkGrid">
<div id="node-S0" class="node" style="--trust:0.5;--node-color:#73a7ff">
<div class="node-id"><span>S0</span><span>0.50</span></div>
<div class="node-trust"><div class="trust-rail"><span></span></div></div>
<div class="node-meta"><span>public slot</span><span>watch</span></div>
</div>
<div id="node-S1" class="node" style="--trust:0.5;--node-color:#73a7ff">
<div class="node-id"><span>S1</span><span>0.50</span></div>
<div class="node-trust"><div class="trust-rail"><span></span></div></div>
<div class="node-meta"><span>public slot</span><span>watch</span></div>
</div>
<div id="node-S2" class="node" style="--trust:0.5;--node-color:#73a7ff">
<div class="node-id"><span>S2</span><span>0.50</span></div>
<div class="node-trust"><div class="trust-rail"><span></span></div></div>
<div class="node-meta"><span>public slot</span><span>watch</span></div>
</div>
<div id="node-S3" class="node" style="--trust:0.5;--node-color:#73a7ff">
<div class="node-id"><span>S3</span><span>0.50</span></div>
<div class="node-trust"><div class="trust-rail"><span></span></div></div>
<div class="node-meta"><span>public slot</span><span>watch</span></div>
</div>
<div id="node-S4" class="node" style="--trust:0.5;--node-color:#73a7ff">
<div class="node-id"><span>S4</span><span>0.50</span></div>
<div class="node-trust"><div class="trust-rail"><span></span></div></div>
<div class="node-meta"><span>public slot</span><span>watch</span></div>
</div>
</div>
<div class="outcome-strip">
<div class="outcome">
<span>Recommended move</span>
<strong id="stageMove">delegate:S0</strong>
</div>
<div class="outcome">
<span>Adversarial signals</span>
<strong id="stageSignals">0 detected / 0 poison</strong>
</div>
<div class="outcome">
<span>Trust objective</span>
<strong>skill, not identity</strong>
</div>
</div>
</div>
</div>
</section>
<section class="command">
<div class="section-head">
<h2>Command Deck</h2>
<span id="sessionText" class="muted">No session</span>
</div>
<div class="body">
<div class="command-grid">
<div class="decision-card">
<div class="decision-title">
<strong>Route Decision</strong>
<span id="recommendChip" class="chip live">delegate:S0</span>
</div>
<select id="specialistSelect" aria-label="Specialist"></select>
<div class="recommendation">
<div id="recommendText" class="recommendation-output">Waiting for episode state.</div>
<button id="applyRecommendBtn" class="primary" type="button">Apply</button>
</div>
</div>
<div class="action-row">
<button id="delegateBtn" class="primary" type="button">Delegate</button>
<button id="verifyBtn" class="warn" type="button">Verify</button>
<button id="selfBtn" type="button">Self Solve</button>
<button id="skipBtn" class="danger" type="button">Skip</button>
</div>
<div class="button-row">
<button id="resetPanelBtn" type="button">New Seed Run</button>
<button id="swapPanelBtn" class="warn" type="button">Profile Swap</button>
</div>
</div>
</div>
</section>
<section class="mission">
<div class="section-head">
<h2>Mission State</h2>
<div class="chips">
<span id="detectChip" class="chip live">0 detected</span>
<span id="poisonChip" class="chip warn">0 poison</span>
</div>
</div>
<div class="body">
<div class="stats-grid">
<div class="stat">
<div class="label">Score</div>
<div id="scoreValue" class="value">0.000</div>
</div>
<div class="stat">
<div class="label">Step Budget</div>
<div id="stepValue" class="value">0/45</div>
</div>
<div class="stat">
<div class="label">Subtasks Done</div>
<div id="completeValue" class="value">0/20</div>
</div>
<div class="stat">
<div class="label">Stakes</div>
<div id="stakesValue" class="value">0.00</div>
</div>
</div>
<div class="progress"><span id="progressFill"></span></div>
<div class="risk-meter">
<div class="label">Risk gate</div>
<div class="risk-track"><span id="riskFill"></span></div>
</div>
<div id="subtaskText" class="subtask">Reset an episode to begin.</div>
</div>
</section>
<section class="trust">
<div class="section-head">
<h2>Bayesian Trust Ledger</h2>
<span id="trustMean" class="muted">mean 0.50</span>
</div>
<div class="body">
<div id="trustList" class="trust-list"></div>
</div>
</section>
<section class="playground">
<div class="section-head">
<h2>API Playground</h2>
<div class="chips">
<span id="endpointChip" class="chip">POST /reset</span>
<span class="chip live">backend visible</span>
</div>
</div>
<div class="body">
<div class="json-grid">
<div class="json-panel">
<div class="json-head">
<strong>Last Request</strong>
<span>what UI sent</span>
</div>
<pre id="requestJson" class="json-block">{
"status": "waiting",
"message": "Reset or step to inspect backend payloads."
}</pre>
</div>
<div class="json-panel">
<div class="json-head">
<strong>Last Response</strong>
<span>what backend returned</span>
</div>
<pre id="responseJson" class="json-block">{
"status": "waiting",
"message": "Observation, reward, and info will appear here."
}</pre>
</div>
</div>
<div class="playground-meta">
<div class="playground-card">
<strong id="playgroundMode">reset()</strong>
<span id="playgroundCopy">Start a new episode, get the first observation, then choose actions step by step.</span>
</div>
<div class="playground-card">
<strong>What To Watch</strong>
<span id="playgroundSummary">Observation tells you the next subtask and public trust scores. Reward tells you whether routing was correct. Info tells you the normalized episode score and adversarial counters.</span>
</div>
</div>
</div>
</section>
<section class="story">
<div class="section-head">
<h2>Before And After</h2>
<div class="chips">
<span class="chip fail">blind trust</span>
<span class="chip live">trained skepticism</span>
</div>
</div>
<div class="body">
<div class="story-grid">
<div class="story-lane before">
<div class="story-title">
<strong>Without SENTINEL</strong>
<span id="storyBeforeScore" class="story-score">task3 random 0.666</span>
</div>
<div class="story-flow">
<div class="story-step">All public slots start near the same trust. The orchestrator delegates with weak evidence.</div>
<div class="story-step">A high-confidence specialist can slip poisoned output into a high-stakes node.</div>
<div class="story-step">Downstream subtasks inherit bad state, so the mission drifts before anyone notices.</div>
<div class="story-step">Detection stays weak and the agent cannot explain which public slot became dangerous.</div>
</div>
<div class="story-note">Judge takeaway: good-looking multi-agent systems still fail if trust is static or role-based.</div>
</div>
<div class="story-lane after">
<div class="story-title">
<strong>With SENTINEL</strong>
<span id="storyAfterScore" class="story-score">task3 heuristic 0.784</span>
</div>
<div class="story-flow">
<div class="story-step">Behavior updates the TrustLedger after every step, so public slots diverge quickly.</div>
<div class="story-step">When stakes rise and trust is shaky, the orchestrator switches from delegate to verify.</div>
<div class="story-step">Adversarial attempts are detected before they cascade through the task graph.</div>
<div class="story-step">Profile swap forces re-learning from evidence, proving skill instead of memorized identity.</div>
</div>
<div class="story-note">Judge takeaway: this environment teaches oversight, recovery, and calibrated delegation under uncertainty.</div>
</div>
</div>
</div>
</section>
<section class="judge">
<div class="section-head">
<h2>Judge Demo Rail</h2>
<div class="chips">
<span class="chip live">3-minute flow</span>
<span class="chip">one-click policies</span>
</div>
</div>
<div class="body">
<div class="judge-grid">
<div class="judge-stats">
<div class="judge-card bad">
<div class="label">Random baseline</div>
<div id="judgeRandomScore" class="value">0.695</div>
<span class="muted">Blind delegation baseline. Good enough to move, weak at skepticism.</span>
</div>
<div class="judge-card warn">
<div class="label">Heuristic policy</div>
<div id="judgeHeuristicScore" class="value">0.796</div>
<span class="muted">Trust-weighted routing plus verification at risky gates.</span>
</div>
<div class="judge-card good">
<div class="label">Task 3 detection</div>
<div id="judgeDetectionScore" class="value">0.735</div>
<span class="muted">Adversarial detections before poison can cascade into later nodes.</span>
</div>
</div>
<div class="judge-actions">
<button id="randomPolicyBtn" class="danger" type="button">Run Random</button>
<button id="heuristicPolicyBtn" class="primary" type="button">Run Heuristic</button>
<button id="judgeSwapBtn" class="warn" type="button">Swap + Replay</button>
</div>
<div class="judge-list">
<div class="judge-step">
<strong>Step 1 — show the failure</strong>
Run Random to show how similar-looking trust scores lead to brittle routing and weak detection.
</div>
<div class="judge-step">
<strong>Step 2 — show the learned behavior</strong>
Run Heuristic to show trust divergence, verification at risky gates, and cleaner recovery.
</div>
<div class="judge-step">
<strong>Step 3 — show generalization</strong>
Hit Swap + Replay so hidden roles reshuffle and the orchestrator has to learn from fresh evidence again.
</div>
</div>
</div>
</div>
</section>
<section class="readiness">
<div class="section-head">
<h2>Hackathon Readiness</h2>
<span class="muted">what is done vs what is left</span>
</div>
<div class="body">
<div class="readiness-list">
<div class="readiness-item">
<strong>Environment Core Ready</strong>
<span>OpenEnv shape works: reset, step, state, normalized score, Docker, Space, and live dashboard.</span>
</div>
<div class="readiness-item">
<strong>Reward Proof Ready</strong>
<span>Random, heuristic, and oracle-lite comparisons are committed and visible in the UI.</span>
</div>
<div class="readiness-item">
<strong>Training Harness Ready</strong>
<span>TRL and Unsloth dry-run path exists; onsite job is to capture the real reward-improvement curve.</span>
</div>
<div class="readiness-item pending">
<strong>Still Needed For Finale</strong>
<span>Mini-blog or video, onsite GRPO run, and one polished 3-minute story using this dashboard plus before/after evidence.</span>
</div>
</div>
</div>
</section>
<section class="proof">
<div class="section-head">
<h2>Reward Signal Proof</h2>
<span class="muted">random to heuristic to oracle-lite</span>
</div>
<div class="body">
<div class="proof-grid">
<div class="baseline-table">
<div class="baseline-row">
<span>Random</span>
<div class="mini-bar"><span id="proofRandomBar" style="width:69.5%;background:#ff5f45"></span></div>
<strong id="proofRandomScore">0.695</strong>
</div>
<div class="baseline-row">
<span>Heuristic</span>
<div class="mini-bar"><span id="proofHeuristicBar" style="width:79.6%;background:#73a7ff"></span></div>
<strong id="proofHeuristicScore">0.796</strong>
</div>
<div class="baseline-row">
<span>Oracle-lite</span>
<div class="mini-bar"><span id="proofOracleBar" style="width:85.5%;background:#27e0a1"></span></div>
<strong id="proofOracleScore">0.855</strong>
</div>
<div class="baseline-row">
<span>T3 detect</span>
<div class="mini-bar"><span id="proofDetectBar" style="width:73.5%;background:#f5ba41"></span></div>
<strong id="proofDetectScore">0.735</strong>
</div>
</div>
<div class="chart-frame">
<img src="/assets/baseline_comparison.png" alt="SENTINEL baseline comparison chart">
</div>
</div>
</div>
</section>
<section class="events">
<div class="section-head">
<h2>Flight Recorder</h2>
<span id="rewardText" class="muted">last reward 0.00</span>
</div>
<div class="body">
<div id="eventList" class="event-list"></div>
</div>
</section>
<section class="flow">
<div class="section-head">
<h2>Code Flow</h2>
<span class="muted">reset, step, state</span>
</div>
<div class="body">
<div class="flow-line">
<div class="flow-step" data-num="1">
<strong>Reset</strong>
<span>environment.py samples scenario, resets graph, ledger, and specialist profile.</span>
</div>
<div class="flow-step" data-num="2">
<strong>Observe</strong>
<span>agent sees subtask, stakes, trust snapshot, step budget, and public slots.</span>
</div>
<div class="flow-step" data-num="3">
<strong>Act</strong>
<span>delegate, verify, self solve, or skip through the OpenEnv step API.</span>
</div>
<div class="flow-step" data-num="4">
<strong>Specialist</strong>
<span>scripted FSM returns outcome, confidence, cost, and adversarial flag.</span>
</div>
<div class="flow-step" data-num="5">
<strong>Ledger</strong>
<span>trust_ledger.py updates public slot reliability from observed behavior.</span>
</div>
<div class="flow-step" data-num="6">
<strong>Reward</strong>
<span>graders.py scores completion, detection, calibration, and efficiency.</span>
</div>
</div>
</div>
</section>
<section class="themes">
<div class="section-head">
<h2>Hackathon Fit</h2>
<span class="muted">judge story map</span>
</div>
<div class="body">
<div class="theme-grid">
<div class="theme-card blue">
<strong>Theme 1</strong>
<span>Orchestrator manages five partially observable actors under adversarial pressure.</span>
</div>
<div class="theme-card green">
<strong>Theme 2</strong>
<span>Long-horizon task graph with budget pressure, retries, and delayed terminal reward.</span>
</div>
<div class="theme-card amber">
<strong>Theme 4</strong>
<span>Profile shuffle creates an adaptive curriculum and blocks identity memorization.</span>
</div>
<div class="theme-card magenta">
<strong>Wild Card</strong>
<span>Turns blind agent-to-agent trust into a trainable safety and oversight skill.</span>
</div>
</div>
</div>
</section>
</main>
</div>
<script>
const ids = ["S0", "S1", "S2", "S3", "S4"];
const state = {
sessionId: null,
taskType: "task3",
observation: null,
done: true,
running: false,
events: [],
lastRequest: null,
lastResult: null,
lastMode: "reset()",
view: "overview",
evaluation: null,
demoPolicy: "heuristic"
};
const el = {
console: document.getElementById("console"),
taskSelect: document.getElementById("taskSelect"),
seedInput: document.getElementById("seedInput"),
resetBtn: document.getElementById("resetBtn"),
resetPanelBtn: document.getElementById("resetPanelBtn"),
swapBtn: document.getElementById("swapBtn"),
swapPanelBtn: document.getElementById("swapPanelBtn"),
autoBtn: document.getElementById("autoBtn"),
viewOverviewBtn: document.getElementById("viewOverviewBtn"),
viewPlaygroundBtn: document.getElementById("viewPlaygroundBtn"),
viewJudgeBtn: document.getElementById("viewJudgeBtn"),
viewCopy: document.getElementById("viewCopy"),
randomPolicyBtn: document.getElementById("randomPolicyBtn"),
heuristicPolicyBtn: document.getElementById("heuristicPolicyBtn"),
judgeSwapBtn: document.getElementById("judgeSwapBtn"),
specialistSelect: document.getElementById("specialistSelect"),
recommendChip: document.getElementById("recommendChip"),
recommendText: document.getElementById("recommendText"),
applyRecommendBtn: document.getElementById("applyRecommendBtn"),
delegateBtn: document.getElementById("delegateBtn"),
verifyBtn: document.getElementById("verifyBtn"),
selfBtn: document.getElementById("selfBtn"),
skipBtn: document.getElementById("skipBtn"),
statusChip: document.getElementById("statusChip"),
scenarioChip: document.getElementById("scenarioChip"),
scoreValue: document.getElementById("scoreValue"),
stepValue: document.getElementById("stepValue"),
completeValue: document.getElementById("completeValue"),
stakesValue: document.getElementById("stakesValue"),
progressFill: document.getElementById("progressFill"),
riskFill: document.getElementById("riskFill"),
subtaskText: document.getElementById("subtaskText"),
trustList: document.getElementById("trustList"),
trustMean: document.getElementById("trustMean"),
endpointChip: document.getElementById("endpointChip"),
requestJson: document.getElementById("requestJson"),
responseJson: document.getElementById("responseJson"),
playgroundMode: document.getElementById("playgroundMode"),
playgroundCopy: document.getElementById("playgroundCopy"),
playgroundSummary: document.getElementById("playgroundSummary"),
detectChip: document.getElementById("detectChip"),
poisonChip: document.getElementById("poisonChip"),
sessionText: document.getElementById("sessionText"),
eventList: document.getElementById("eventList"),
leadMove: document.getElementById("leadMove"),
stageMove: document.getElementById("stageMove"),
stageSignals: document.getElementById("stageSignals"),
rewardText: document.getElementById("rewardText"),
heroRandomScore: document.getElementById("heroRandomScore"),
heroHeuristicScore: document.getElementById("heroHeuristicScore"),
heroDetectionScore: document.getElementById("heroDetectionScore"),
storyBeforeScore: document.getElementById("storyBeforeScore"),
storyAfterScore: document.getElementById("storyAfterScore"),
judgeRandomScore: document.getElementById("judgeRandomScore"),
judgeHeuristicScore: document.getElementById("judgeHeuristicScore"),
judgeDetectionScore: document.getElementById("judgeDetectionScore"),
proofRandomBar: document.getElementById("proofRandomBar"),
proofRandomScore: document.getElementById("proofRandomScore"),
proofHeuristicBar: document.getElementById("proofHeuristicBar"),
proofHeuristicScore: document.getElementById("proofHeuristicScore"),
proofOracleBar: document.getElementById("proofOracleBar"),
proofOracleScore: document.getElementById("proofOracleScore"),
proofDetectBar: document.getElementById("proofDetectBar"),
proofDetectScore: document.getElementById("proofDetectScore")
};
function trustColor(value) {
if (value >= 0.72) return "#63dfc0";
if (value >= 0.48) return "#8eb5ff";
if (value >= 0.30) return "#ffbf7d";
return "#ff8d93";
}
function trustStatus(value) {
if (value >= 0.72) return "trusted";
if (value >= 0.48) return "watch";
if (value >= 0.30) return "risk";
return "quarantine";
}
function prettyJson(value) {
return JSON.stringify(value, null, 2);
}
function bestSpecialist() {
const obs = state.observation;
if (!obs) return "S0";
return obs.available_specialists
.slice()
.sort((a, b) => (obs.trust_snapshot[b] ?? 0.5) - (obs.trust_snapshot[a] ?? 0.5))[0];
}
function recommendedMove() {
const obs = state.observation;
if (!obs) return {type: "delegate", specialist: "S0", trust: 0.5};
const best = bestSpecialist();
const trust = obs.trust_snapshot[best] ?? 0.5;
const highStakes = obs.stakes_level >= 0.70;
if (highStakes && trust < 0.65) return {type: "verify", specialist: best, trust};
return {type: "delegate", specialist: best, trust};
}
function randomMove() {
const obs = state.observation;
if (!obs) return {type: "delegate", specialist: "S0", trust: 0.5};
const available = obs.available_specialists || ids;
const specialist = available[Math.floor(Math.random() * available.length)] || "S0";
return {type: "delegate", specialist, trust: obs.trust_snapshot?.[specialist] ?? 0.5};
}
function setView(view) {
state.view = view;
const sectionViews = {
hero: ["overview", "judge"],
theater: ["playground", "judge"],
command: ["playground"],
mission: ["playground", "judge"],
trust: ["playground"],
playground: ["playground"],
story: ["overview", "judge"],
judge: ["judge"],
readiness: ["overview"],
proof: ["overview", "judge"],
events: ["playground"],
flow: ["overview"],
themes: ["overview"]
};
Object.entries(sectionViews).forEach(([name, views]) => {
const node = document.querySelector(`section.${name}`);
if (!node) return;
node.classList.toggle("section-hidden", !views.includes(view));
});
el.viewOverviewBtn.classList.toggle("active", view === "overview");
el.viewPlaygroundBtn.classList.toggle("active", view === "playground");
el.viewJudgeBtn.classList.toggle("active", view === "judge");
if (el.console) {
el.console.dataset.view = view;
}
const copy = {
overview: "Overview is the clean narrative surface: what SENTINEL solves, how reward proves learning, and why trust calibration matters for real multi-agent systems.",
playground: "Playground is the engineering surface: live trust state, action routing, backend request and response payloads, and flight-recorder events.",
judge: "Judge Demo is the pitch surface: baseline failure, heuristic recovery, then profile swap to show the orchestrator learned a transferable trust skill."
};
if (el.viewCopy) {
el.viewCopy.textContent = copy[view] || copy.overview;
}
}
async function loadEvaluation() {
try {
const response = await fetch("/assets/evaluation_results.json");
if (!response.ok) throw new Error("evaluation asset missing");
state.evaluation = await response.json();
renderEvaluation();
} catch (error) {
console.warn("Failed to load evaluation results", error);
}
}
function setMetricText(node, value, digits = 3) {
if (!node || value === undefined || value === null || Number.isNaN(Number(value))) return;
node.textContent = Number(value).toFixed(digits);
}
function setMetricBar(node, value) {
if (!node || value === undefined || value === null || Number.isNaN(Number(value))) return;
node.style.width = `${Math.max(0, Math.min(100, Number(value) * 100))}%`;
}
function renderEvaluation() {
const data = state.evaluation;
if (!data) return;
const overall = data.summary || {};
const task3 = data.by_task?.task3 || {};
const random = overall.random || {};
const heuristic = overall.heuristic || {};
const oracle = overall.oracle_lite || {};
const task3Random = task3.random || {};
const task3Heuristic = task3.heuristic || {};
setMetricText(el.heroRandomScore, random.avg_score);
setMetricText(el.heroHeuristicScore, heuristic.avg_score);
setMetricText(el.heroDetectionScore, task3Heuristic.avg_detection_rate);
if (el.storyBeforeScore && task3Random.avg_score !== undefined) {
el.storyBeforeScore.textContent = `task3 random ${Number(task3Random.avg_score).toFixed(3)}`;
}
if (el.storyAfterScore && task3Heuristic.avg_score !== undefined) {
el.storyAfterScore.textContent = `task3 heuristic ${Number(task3Heuristic.avg_score).toFixed(3)}`;
}
setMetricText(el.judgeRandomScore, random.avg_score);
setMetricText(el.judgeHeuristicScore, heuristic.avg_score);
setMetricText(el.judgeDetectionScore, task3Heuristic.avg_detection_rate);
setMetricBar(el.proofRandomBar, random.avg_score);
setMetricBar(el.proofHeuristicBar, heuristic.avg_score);
setMetricBar(el.proofOracleBar, oracle.avg_score);
setMetricBar(el.proofDetectBar, task3Heuristic.avg_detection_rate);
setMetricText(el.proofRandomScore, random.avg_score);
setMetricText(el.proofHeuristicScore, heuristic.avg_score);
setMetricText(el.proofOracleScore, oracle.avg_score);
setMetricText(el.proofDetectScore, task3Heuristic.avg_detection_rate);
}
function renderTrust() {
const trust = state.observation?.trust_snapshot || Object.fromEntries(ids.map(id => [id, 0.5]));
const values = ids.map(id => Number(trust[id] ?? 0.5));
const mean = values.reduce((sum, value) => sum + value, 0) / values.length;
el.trustMean.textContent = `mean ${mean.toFixed(2)}`;
el.trustList.innerHTML = ids.map(id => {
const value = Number(trust[id] ?? 0.5);
const pct = Math.round(value * 100);
const color = trustColor(value);
return `
<div class="specialist">
<div class="sid">${id}</div>
<div class="bar"><div class="fill" style="width:${pct}%;background:${color}"></div></div>
<div class="score">${value.toFixed(2)}</div>
</div>
`;
}).join("");
ids.forEach(id => {
const node = document.getElementById(`node-${id}`);
const value = Number(trust[id] ?? 0.5);
const status = trustStatus(value);
const active = id === bestSpecialist();
node.style.setProperty("--trust", value);
node.style.setProperty("--node-color", trustColor(value));
node.className = `node ${active ? "active" : ""} ${status === "risk" ? "watch" : ""} ${status === "quarantine" ? "quarantine" : ""}`;
node.querySelector(".node-id").innerHTML = `<span>${id}</span><span>${value.toFixed(2)}</span>`;
node.querySelector(".node-meta").innerHTML = `<span>public slot</span><span>${status}</span>`;
});
}
function renderSpecialists() {
const available = state.observation?.available_specialists || ids;
const selected = el.specialistSelect.value || bestSpecialist();
el.specialistSelect.innerHTML = available.map(id => `<option value="${id}">${id}</option>`).join("");
el.specialistSelect.value = available.includes(selected) ? selected : bestSpecialist();
}
function renderEvents() {
if (!state.events.length) {
el.eventList.innerHTML = `<div class="muted">No events yet.</div>`;
return;
}
el.eventList.innerHTML = state.events.slice(-18).reverse().map(item => `
<div class="event">
<strong>#${item.step}</strong>
<div>${item.action}<br><span class="muted">${item.summary}</span></div>
<div class="reward">${item.reward}</div>
</div>
`).join("");
}
function renderPlayground() {
if (el.requestJson) {
el.requestJson.textContent = prettyJson(state.lastRequest || {
status: "waiting",
message: "Reset or step to inspect backend payloads."
});
}
if (el.responseJson) {
el.responseJson.textContent = prettyJson(state.lastResult || {
status: "waiting",
message: "Observation, reward, and info will appear here."
});
}
if (el.playgroundMode) {
el.playgroundMode.textContent = state.lastMode;
}
if (el.endpointChip) {
const path = state.lastRequest?.path || "/reset";
el.endpointChip.textContent = `POST ${path}`;
}
if (el.playgroundCopy) {
el.playgroundCopy.textContent = state.lastMode === "step()"
? "A step sends one action into the environment and returns the next observation, reward, done flag, and info."
: "Reset starts a new episode, samples a scenario, reshuffles hidden profiles, and returns the first observation.";
}
if (el.playgroundSummary) {
const obs = state.lastResult?.observation;
const reward = state.lastResult?.reward;
const info = state.lastResult?.info;
el.playgroundSummary.textContent = obs
? `Current subtask: ${obs.current_subtask} | Reward: ${Number(reward?.value ?? 0).toFixed(2)} | Score: ${Number(info?.score ?? 0).toFixed(3)} | Detections: ${info?.adversarial_detections ?? 0}`
: "Observation tells you the next subtask and public trust scores. Reward tells you whether routing was correct. Info tells you the normalized episode score and adversarial counters.";
}
}
function renderRecommendation() {
const move = recommendedMove();
const obs = state.observation;
const label = `${move.type}:${move.specialist}`;
const highStakes = Number(obs?.stakes_level ?? 0) >= 0.70;
el.recommendChip.textContent = label;
el.recommendChip.className = `chip ${move.type === "verify" ? "warn" : "live"}`;
el.stageMove.textContent = label;
el.leadMove.textContent = obs
? `${move.type.toUpperCase()} ${move.specialist} at trust ${move.trust.toFixed(2)}`
: "Reset starts a fresh trust game.";
el.recommendText.textContent = highStakes
? `High-stakes gate active. ${move.type === "verify" ? "Verify before accepting output." : "Best specialist is trusted enough to delegate."}`
: `Route to ${move.specialist}; keep budget for later high-stakes checks.`;
}
function setDisabled(disabled) {
el.delegateBtn.disabled = disabled;
el.verifyBtn.disabled = disabled;
el.selfBtn.disabled = disabled;
el.skipBtn.disabled = disabled;
el.applyRecommendBtn.disabled = disabled;
if (el.randomPolicyBtn) el.randomPolicyBtn.disabled = state.running;
if (el.heuristicPolicyBtn) el.heuristicPolicyBtn.disabled = state.running;
if (el.judgeSwapBtn) el.judgeSwapBtn.disabled = state.running;
}
function render(result) {
if (result) {
state.observation = result.observation;
state.done = Boolean(result.done);
}
const obs = state.observation;
if (!obs) {
renderTrust();
renderSpecialists();
renderEvents();
renderRecommendation();
renderPlayground();
setDisabled(true);
return;
}
const info = result?.info || {};
const completed = obs.subtasks_total - obs.subtasks_remaining;
const progress = obs.subtasks_total ? (completed / obs.subtasks_total) * 100 : 0;
const status = state.done ? "DONE" : obs.episode_status.toUpperCase();
const detections = info.adversarial_detections ?? 0;
const poisonings = info.adversarial_poisonings ?? 0;
const lastReward = Number(result?.reward?.value ?? obs.last_reward ?? 0);
el.statusChip.textContent = status;
el.statusChip.className = `chip ${state.done ? "live" : "live"}`;
el.scenarioChip.textContent = obs.scenario_id;
el.scoreValue.textContent = Number(info.score ?? 0).toFixed(3);
el.stepValue.textContent = `${obs.step_count}/${obs.max_steps}`;
el.completeValue.textContent = `${completed}/${obs.subtasks_total}`;
el.stakesValue.textContent = Number(obs.stakes_level).toFixed(2);
el.progressFill.style.width = `${Math.max(0, Math.min(100, progress))}%`;
el.riskFill.style.width = `${Math.round(Number(obs.stakes_level || 0) * 100)}%`;
el.subtaskText.textContent = state.done ? "Episode complete. Swap profiles for the generalization demo." : obs.current_subtask;
el.sessionText.textContent = state.sessionId ? `session ${state.sessionId.slice(0, 8)}` : "No session";
el.detectChip.textContent = `${detections} detected`;
el.poisonChip.textContent = `${poisonings} poison`;
el.poisonChip.className = `chip ${poisonings > 0 ? "fail" : "warn"}`;
el.stageSignals.textContent = `${detections} detected / ${poisonings} poison`;
el.rewardText.textContent = `last reward ${lastReward.toFixed(2)}`;
renderTrust();
renderSpecialists();
renderEvents();
renderRecommendation();
renderPlayground();
setDisabled(state.done || state.running);
}
function addEvent(step, action, summary, reward) {
state.events.push({step, action, summary, reward});
}
function actionPayload(type, specialist) {
const obs = state.observation;
return {
session_id: state.sessionId,
task_type: obs.task_type,
action_type: type,
specialist_id: specialist,
subtask_response: type === "solve_independently" ? "SELF_SOLVED" : null,
reasoning: `ui-${type}${specialist ? "-" + specialist : ""}`
};
}
async function resetEpisode() {
state.running = true;
el.resetBtn.disabled = true;
el.resetPanelBtn.disabled = true;
try {
const seed = Number(el.seedInput.value || 0);
state.lastMode = "reset()";
state.lastRequest = {
method: "POST",
path: "/reset",
body: {task_type: el.taskSelect.value, seed}
};
const response = await fetch("/reset", {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify({task_type: el.taskSelect.value, seed})
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || "reset failed");
state.lastResult = result;
state.taskType = result.observation.task_type;
state.sessionId = result.info.session_id;
state.events = [];
state.done = false;
addEvent(0, "reset", "Episode initialized with shuffled hidden profiles.", "0.00");
render(result);
} catch (error) {
state.lastResult = {error: error.message};
addEvent(0, "error", error.message, "0.00");
renderPlayground();
renderEvents();
} finally {
state.running = false;
el.resetBtn.disabled = false;
el.resetPanelBtn.disabled = false;
setDisabled(state.done);
}
}
async function stepEpisode(type, specialist = null) {
if (!state.sessionId || state.done || state.running) return;
state.running = true;
setDisabled(true);
try {
const chosen = specialist || el.specialistSelect.value || bestSpecialist();
const payload = actionPayload(type, type === "delegate" || type === "verify" ? chosen : null);
state.lastMode = "step()";
state.lastRequest = {
method: "POST",
path: `/step?session_id=${state.sessionId}`,
body: payload
};
const response = await fetch(`/step?session_id=${encodeURIComponent(state.sessionId)}`, {
method: "POST",
headers: {"Content-Type": "application/json"},
body: JSON.stringify(payload)
});
const result = await response.json();
if (!response.ok) throw new Error(result.detail || "step failed");
state.lastResult = result;
const reward = Number(result.reward.value || 0).toFixed(2);
const label = payload.specialist_id ? `${type}:${payload.specialist_id}` : type;
addEvent(result.info.step_count, label, result.reward.reason, reward);
render(result);
} catch (error) {
state.lastResult = {error: error.message};
addEvent(state.observation?.step_count || 0, "error", error.message, "0.00");
renderPlayground();
renderEvents();
} finally {
state.running = false;
setDisabled(state.done);
}
}
async function autoRun(policy = state.demoPolicy) {
if (!state.observation || state.done) await resetEpisode();
let guard = 0;
while (!state.done && guard < 70) {
const move = policy === "random" ? randomMove() : recommendedMove();
await stepEpisode(move.type, move.specialist);
guard += 1;
await new Promise(resolve => setTimeout(resolve, 150));
}
}
async function applyRecommendation() {
const move = recommendedMove();
await stepEpisode(move.type, move.specialist);
}
async function runPolicy(policy) {
state.demoPolicy = policy;
await resetEpisode();
await autoRun(policy);
}
async function swapProfiles(policy = null) {
const nextSeed = Number(el.seedInput.value || 0) + 1;
el.seedInput.value = String(nextSeed);
await resetEpisode();
if (policy) {
await autoRun(policy);
}
}
el.resetBtn.addEventListener("click", resetEpisode);
el.resetPanelBtn.addEventListener("click", resetEpisode);
el.swapBtn.addEventListener("click", () => swapProfiles());
el.swapPanelBtn.addEventListener("click", () => swapProfiles());
el.delegateBtn.addEventListener("click", () => stepEpisode("delegate"));
el.verifyBtn.addEventListener("click", () => stepEpisode("verify"));
el.selfBtn.addEventListener("click", () => stepEpisode("solve_independently"));
el.skipBtn.addEventListener("click", () => stepEpisode("skip"));
el.autoBtn.addEventListener("click", () => autoRun("heuristic"));
el.applyRecommendBtn.addEventListener("click", applyRecommendation);
el.viewOverviewBtn.addEventListener("click", () => setView("overview"));
el.viewPlaygroundBtn.addEventListener("click", () => setView("playground"));
el.viewJudgeBtn.addEventListener("click", () => setView("judge"));
el.randomPolicyBtn.addEventListener("click", () => runPolicy("random"));
el.heuristicPolicyBtn.addEventListener("click", () => runPolicy("heuristic"));
el.judgeSwapBtn.addEventListener("click", () => swapProfiles(state.demoPolicy));
setView("overview");
loadEvaluation();
render();
resetEpisode();
</script>
</body>
</html>