yahid's picture
Upload folder using huggingface_hub
d30f66b verified
Raw
History Blame Contribute Delete
56.5 kB
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Triage Agent Β· Enterprise IT RL Environment</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,500;0,700;1,400&family=Inter:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
<style>
/* ═══════════════════════════════════════════
CSS Variables & Reset
═══════════════════════════════════════════ */
:root {
--bg: #070d1a;
--surface: #0d1929;
--surface2: #112035;
--border: #1a2d4a;
--border2: #243d5e;
--accent: #00d4ff;
--accent-dim: rgba(0, 212, 255, 0.15);
--accent-glow: rgba(0, 212, 255, 0.4);
--success: #00ff88;
--success-dim: rgba(0, 255, 136, 0.12);
--warning: #ffaa00;
--warning-dim: rgba(255, 170, 0, 0.12);
--danger: #ff4444;
--danger-dim: rgba(255, 68, 68, 0.12);
--text: #e8f4f8;
--text2: #b0cce0;
--muted: #5a7a9a;
--muted2: #3a5272;
--font-mono: 'JetBrains Mono', 'Fira Code', monospace;
--font-sans: 'Inter', system-ui, sans-serif;
}
*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
html { scroll-behavior: smooth; }
body {
background: var(--bg);
color: var(--text);
font-family: var(--font-sans);
line-height: 1.6;
overflow-x: hidden;
}
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
/* ═══ Scrollbar ═══ */
::-webkit-scrollbar { width: 6px; }
::-webkit-scrollbar-track { background: var(--bg); }
::-webkit-scrollbar-thumb { background: var(--border2); border-radius: 3px; }
/* ═══════════════════════════════════════════
NAV
═══════════════════════════════════════════ */
nav {
position: sticky;
top: 0;
z-index: 100;
background: rgba(7, 13, 26, 0.92);
backdrop-filter: blur(12px);
border-bottom: 1px solid var(--border);
padding: 0 2rem;
height: 56px;
display: flex;
align-items: center;
justify-content: space-between;
}
.nav-brand {
display: flex;
align-items: center;
gap: 10px;
font-weight: 700;
font-size: 0.95rem;
letter-spacing: 0.02em;
}
.nav-brand .dot-badge {
width: 8px; height: 8px;
background: var(--success);
border-radius: 50%;
animation: pulse-green 2s infinite;
}
.nav-links {
display: flex;
gap: 1.5rem;
font-size: 0.82rem;
color: var(--muted);
}
.nav-links a { color: var(--muted); }
.nav-links a:hover { color: var(--text); text-decoration: none; }
/* ═══════════════════════════════════════════
SECTION 1 β€” HERO
═══════════════════════════════════════════ */
#hero {
padding: 3rem 2rem 0;
max-width: 1400px;
margin: 0 auto;
}
.hero-title {
text-align: center;
margin-bottom: 2.5rem;
}
.hero-title h1 {
font-size: 2.2rem;
font-weight: 800;
letter-spacing: -0.03em;
background: linear-gradient(135deg, var(--text) 0%, var(--accent) 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}
.hero-title p {
margin-top: 0.5rem;
color: var(--muted);
font-size: 0.9rem;
}
.hero-grid {
display: grid;
grid-template-columns: 1fr 380px;
gap: 2rem;
align-items: start;
}
/* Reward curve chart */
.chart-wrapper {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.25rem 1rem 1rem;
}
.chart-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.75rem;
}
.chart-header h3 {
font-size: 0.85rem;
font-weight: 600;
color: var(--text2);
text-transform: uppercase;
letter-spacing: 0.08em;
}
.chart-legend {
display: flex;
flex-wrap: wrap;
gap: 0.5rem 1rem;
justify-content: flex-end;
}
.legend-item {
display: flex;
align-items: center;
gap: 5px;
font-size: 0.72rem;
color: var(--muted);
font-family: var(--font-mono);
}
.legend-dot {
width: 8px; height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
#reward-svg {
width: 100%;
height: auto;
display: block;
}
/* Stat cards */
.stat-cards {
display: flex;
flex-direction: column;
gap: 1rem;
}
.stat-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
padding: 1.1rem 1.25rem;
position: relative;
overflow: hidden;
transition: border-color 0.3s, box-shadow 0.3s;
}
.stat-card:hover {
border-color: var(--accent);
box-shadow: 0 0 20px rgba(0,212,255,0.08);
}
.stat-card::before {
content: '';
position: absolute;
top: 0; left: 0; right: 0;
height: 2px;
background: linear-gradient(90deg, transparent, var(--accent), transparent);
opacity: 0;
transition: opacity 0.3s;
}
.stat-card:hover::before { opacity: 1; }
.stat-top {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 0.5rem;
}
.stat-name {
font-size: 0.75rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
.stat-sparkline {
width: 80px;
height: 30px;
}
.stat-numbers {
display: flex;
align-items: baseline;
gap: 0.5rem;
}
.stat-before {
font-family: var(--font-mono);
font-size: 1.1rem;
color: var(--muted);
text-decoration: line-through;
}
.stat-arrow { color: var(--success); font-size: 1.2rem; }
.stat-after {
font-family: var(--font-mono);
font-size: 1.8rem;
font-weight: 700;
color: var(--text);
}
.stat-bottom {
display: flex;
justify-content: space-between;
align-items: center;
margin-top: 0.35rem;
}
.stat-subtitle {
font-size: 0.78rem;
color: var(--muted);
font-style: italic;
}
.stat-delta {
font-size: 0.72rem;
font-family: var(--font-mono);
color: var(--success);
font-weight: 600;
}
/* Info banner */
.info-banner {
margin-top: 2rem;
padding: 0.85rem 1.5rem;
background: var(--surface);
border: 1px solid var(--border);
border-radius: 8px;
font-family: var(--font-mono);
font-size: 0.75rem;
color: var(--muted);
text-align: center;
letter-spacing: 0.03em;
}
.info-banner span { color: var(--text2); }
/* ═══════════════════════════════════════════
SECTION 2 β€” BEFORE vs AFTER
═══════════════════════════════════════════ */
#before-after {
margin-top: 4rem;
}
.ba-section-header {
max-width: 1400px;
margin: 0 auto 1.5rem;
padding: 0 2rem;
}
.ba-section-header h2 {
font-size: 1.4rem;
font-weight: 700;
color: var(--text);
}
.ba-section-header p {
font-size: 0.85rem;
color: var(--muted);
margin-top: 0.25rem;
}
.ba-grid {
display: grid;
grid-template-columns: 1fr 100px 1fr;
min-height: 440px;
}
.ba-panel {
padding: 2.5rem 2rem;
}
.ba-before-panel {
background: linear-gradient(135deg, #1a0a0a 0%, #0f0808 100%);
border-top: 1px solid rgba(255,68,68,0.3);
}
.ba-after-panel {
background: linear-gradient(135deg, #0a1a0a 0%, #060f06 100%);
border-top: 1px solid rgba(0,255,136,0.3);
}
.ba-badge {
display: inline-block;
padding: 0.2rem 0.7rem;
border-radius: 4px;
font-size: 0.7rem;
font-family: var(--font-mono);
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
margin-bottom: 1rem;
}
.badge-before {
background: rgba(255,68,68,0.2);
color: var(--danger);
border: 1px solid rgba(255,68,68,0.4);
}
.badge-after {
background: rgba(0,255,136,0.12);
color: var(--success);
border: 1px solid rgba(0,255,136,0.3);
}
.ba-ticket {
margin-bottom: 1.25rem;
}
.ba-ticket-label {
font-size: 0.68rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
margin-bottom: 0.3rem;
}
.ba-ticket p {
font-size: 0.82rem;
color: var(--text2);
line-height: 1.5;
}
.ba-response {
background: rgba(0,0,0,0.3);
border: 1px solid rgba(255,255,255,0.08);
border-radius: 8px;
padding: 1rem;
font-family: var(--font-mono);
font-size: 0.72rem;
line-height: 1.6;
color: #c8e0d8;
min-height: 160px;
overflow-wrap: break-word;
word-break: break-word;
}
.ba-metrics {
display: flex;
gap: 1rem;
margin-top: 1rem;
flex-wrap: wrap;
}
.ba-metric {
display: flex;
align-items: center;
gap: 0.4rem;
font-size: 0.73rem;
font-family: var(--font-mono);
color: var(--muted);
}
.ba-metric span { font-weight: 600; }
.ba-reward {
margin-top: 1rem;
font-size: 1.1rem;
font-weight: 700;
font-family: var(--font-mono);
}
.reward-bad { color: var(--danger); }
.reward-good { color: var(--success); }
/* Center divider */
.ba-divider {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #06080f;
gap: 0.6rem;
border-top: 1px solid var(--border);
}
.ba-arrow-icon {
font-size: 2.8rem;
color: var(--accent);
animation: pulse-arrow 2.5s ease-in-out infinite;
line-height: 1;
}
.ba-steps-label {
font-size: 0.65rem;
font-family: var(--font-mono);
color: var(--muted);
text-align: center;
line-height: 1.4;
}
/* ═══════════════════════════════════════════
SECTION 3 β€” LIVE DEMO
═══════════════════════════════════════════ */
#demo {
max-width: 1400px;
margin: 4rem auto 0;
padding: 0 2rem 4rem;
}
.demo-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1.75rem;
}
.demo-header h2 {
font-size: 1.4rem;
font-weight: 700;
}
.conn-status {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 0.75rem;
font-family: var(--font-mono);
font-weight: 600;
letter-spacing: 0.08em;
}
.status-dot {
width: 8px; height: 8px;
border-radius: 50%;
background: var(--muted);
}
.status-dot.connected {
background: var(--success);
animation: pulse-green 2s infinite;
}
.status-dot.error {
background: var(--danger);
animation: pulse-red 1.5s infinite;
}
#conn-text { color: var(--muted); }
#conn-text.connected { color: var(--success); }
#conn-text.error { color: var(--danger); }
.demo-grid {
display: grid;
grid-template-columns: 340px 1fr 320px;
gap: 1.25rem;
align-items: start;
}
/* Panel base */
.demo-panel {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 12px;
overflow: hidden;
}
.panel-header {
padding: 0.75rem 1rem;
border-bottom: 1px solid var(--border);
background: var(--surface2);
display: flex;
align-items: center;
justify-content: space-between;
}
.panel-title {
font-size: 0.72rem;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
}
.panel-body { padding: 1.1rem; }
/* Ticket panel */
.ticket-id {
font-family: var(--font-mono);
font-size: 0.7rem;
color: var(--muted);
margin-bottom: 0.5rem;
}
.ticket-priority {
display: inline-block;
padding: 0.2rem 0.6rem;
background: rgba(255,68,68,0.2);
color: var(--danger);
border: 1px solid rgba(255,68,68,0.4);
border-radius: 4px;
font-size: 0.68rem;
font-family: var(--font-mono);
font-weight: 700;
letter-spacing: 0.08em;
margin-bottom: 0.75rem;
animation: pulse-priority 2s infinite;
}
.ticket-title {
font-size: 0.95rem;
font-weight: 600;
line-height: 1.4;
margin-bottom: 0.75rem;
color: var(--text);
}
.ticket-desc {
font-size: 0.8rem;
color: var(--text2);
line-height: 1.6;
margin-bottom: 1rem;
}
.ticket-tags {
display: flex;
flex-wrap: wrap;
gap: 0.4rem;
margin-bottom: 1.25rem;
}
.tag {
padding: 0.15rem 0.5rem;
background: var(--accent-dim);
color: var(--accent);
border: 1px solid rgba(0,212,255,0.2);
border-radius: 3px;
font-size: 0.65rem;
font-family: var(--font-mono);
}
.dispatch-btn {
width: 100%;
padding: 0.85rem;
background: linear-gradient(135deg, rgba(0,212,255,0.15), rgba(0,212,255,0.08));
border: 1px solid var(--accent);
border-radius: 8px;
color: var(--accent);
font-size: 0.9rem;
font-weight: 600;
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.2s;
letter-spacing: 0.02em;
}
.dispatch-btn:hover:not(:disabled) {
background: linear-gradient(135deg, rgba(0,212,255,0.25), rgba(0,212,255,0.15));
box-shadow: 0 0 20px var(--accent-glow);
}
.dispatch-btn:disabled {
opacity: 0.4;
cursor: not-allowed;
}
/* Terminal panel */
.terminal {
background: #000a00;
min-height: 320px;
padding: 1rem;
font-family: var(--font-mono);
font-size: 0.73rem;
line-height: 1.7;
color: #33ff33;
}
.terminal-line { display: block; }
.terminal-line.dim { color: #1a8a1a; }
.terminal-line.info { color: #33cc99; }
.terminal-line.success { color: #00ff88; }
.terminal-line.warn { color: #ffaa00; }
.terminal .cursor {
display: inline-block;
width: 8px; height: 14px;
background: #33ff33;
vertical-align: middle;
animation: blink 1s step-end infinite;
}
.terminal-sep {
border: none;
border-top: 1px solid #0a3a0a;
margin: 0.75rem 0;
}
.resolution-text {
color: #ccffee;
font-size: 0.72rem;
line-height: 1.7;
}
/* Reward panel */
.reward-bars {
display: flex;
flex-direction: column;
gap: 0.85rem;
}
.reward-row {
display: flex;
flex-direction: column;
gap: 0.25rem;
}
.reward-row-header {
display: flex;
justify-content: space-between;
align-items: center;
}
.reward-label {
font-size: 0.72rem;
color: var(--muted);
font-family: var(--font-mono);
text-transform: uppercase;
letter-spacing: 0.06em;
}
.reward-value {
font-size: 0.72rem;
font-family: var(--font-mono);
font-weight: 600;
color: var(--muted);
}
.bar-track {
height: 6px;
background: var(--surface2);
border-radius: 3px;
overflow: hidden;
}
.bar-fill {
height: 100%;
width: 0%;
border-radius: 3px;
transition: width 1s cubic-bezier(0.4, 0, 0.2, 1), background 0.5s;
}
/* Ring gauge */
.ring-wrapper {
display: flex;
flex-direction: column;
align-items: center;
margin-top: 1.25rem;
padding-top: 1rem;
border-top: 1px solid var(--border);
}
.ring-label {
font-size: 0.7rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
margin-bottom: 0.75rem;
}
.ring-svg {
width: 120px;
height: 120px;
}
.ring-bg {
fill: none;
stroke: var(--surface2);
stroke-width: 10;
}
.ring-fill {
fill: none;
stroke-width: 10;
stroke-linecap: round;
transform: rotate(-90deg);
transform-origin: 60px 60px;
transition: stroke-dashoffset 1s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.5s;
}
.ring-score-text {
font-family: var(--font-mono);
font-size: 1.5rem;
font-weight: 700;
fill: var(--text);
text-anchor: middle;
dominant-baseline: central;
}
.ring-sublabel {
font-size: 0.68rem;
color: var(--muted);
margin-top: 0.5rem;
}
.next-btn {
width: 100%;
margin-top: 1rem;
padding: 0.6rem;
background: transparent;
border: 1px solid var(--border2);
border-radius: 6px;
color: var(--muted);
font-size: 0.8rem;
font-family: var(--font-sans);
cursor: pointer;
transition: all 0.2s;
}
.next-btn:hover {
border-color: var(--accent);
color: var(--accent);
}
/* ═══════════════════════════════════════════
FOOTER
═══════════════════════════════════════════ */
footer {
background: var(--surface);
border-top: 1px solid var(--border);
padding: 2.5rem 2rem;
}
.footer-inner {
max-width: 1400px;
margin: 0 auto;
display: grid;
grid-template-columns: 1fr 1fr 1fr;
gap: 2rem;
}
.footer-col h4 {
font-size: 0.72rem;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--muted);
margin-bottom: 0.75rem;
}
.footer-col a {
display: block;
font-size: 0.82rem;
color: var(--text2);
margin-bottom: 0.4rem;
}
.footer-col a:hover { color: var(--accent); text-decoration: none; }
.footer-bottom {
max-width: 1400px;
margin: 1.5rem auto 0;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
font-size: 0.75rem;
color: var(--muted);
text-align: center;
font-family: var(--font-mono);
}
/* ═══════════════════════════════════════════
ANIMATIONS
═══════════════════════════════════════════ */
@keyframes pulse-green {
0%, 100% { box-shadow: 0 0 0 0 rgba(0,255,136,0.5); }
50% { box-shadow: 0 0 0 4px rgba(0,255,136,0); }
}
@keyframes pulse-red {
0%, 100% { box-shadow: 0 0 0 0 rgba(255,68,68,0.5); }
50% { box-shadow: 0 0 0 4px rgba(255,68,68,0); }
}
@keyframes pulse-priority {
0%, 100% { opacity: 1; }
50% { opacity: 0.6; }
}
@keyframes pulse-arrow {
0%, 100% { text-shadow: 0 0 8px var(--accent-glow); }
50% { text-shadow: 0 0 24px var(--accent), 0 0 48px var(--accent-glow); }
}
@keyframes blink {
0%, 100% { opacity: 1; }
50% { opacity: 0; }
}
@keyframes fadeInUp {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.fade-in { animation: fadeInUp 0.6s ease both; }
.fade-in-1 { animation-delay: 0.1s; }
.fade-in-2 { animation-delay: 0.2s; }
.fade-in-3 { animation-delay: 0.3s; }
/* SVG chart text */
.chart-axis-label {
font-family: var(--font-mono);
font-size: 10px;
fill: #3a5272;
}
.chart-grid {
stroke: #1a2d4a;
stroke-width: 0.5;
}
.chart-axis {
stroke: #243d5e;
stroke-width: 1;
}
.chart-path {
fill: none;
stroke-width: 2;
stroke-linecap: round;
stroke-linejoin: round;
}
.convergence-line {
stroke: rgba(0,212,255,0.35);
stroke-width: 1;
stroke-dasharray: 4 3;
}
/* ═══════════════════════════════════════════
RESPONSIVE (basic)
═══════════════════════════════════════════ */
@media (max-width: 1100px) {
.hero-grid { grid-template-columns: 1fr; }
.stat-cards { flex-direction: row; flex-wrap: wrap; }
.stat-card { flex: 1; min-width: 200px; }
.demo-grid { grid-template-columns: 1fr; }
}
@media (max-width: 768px) {
.ba-grid { grid-template-columns: 1fr; }
.ba-divider { flex-direction: row; padding: 0.5rem; min-height: unset; }
}
</style>
</head>
<body>
<!-- ═══════════════════════════════════════════
NAV
═══════════════════════════════════════════ -->
<nav>
<div class="nav-brand">
<div class="dot-badge"></div>
Triage Agent &mdash; OpenEnv RL
</div>
<div class="nav-links">
<a href="#hero">Training Story</a>
<a href="#before-after">Before &amp; After</a>
<a href="#demo">Live Demo</a>
<a href="https://huggingface.co/yahid/triage-agent-qwen3b" target="_blank">Model β†—</a>
<a href="https://huggingface.co/spaces/yahid/triage_agent_env" target="_blank">Space β†—</a>
</div>
</nav>
<!-- ═══════════════════════════════════════════
SECTION 1 β€” HERO
═══════════════════════════════════════════ -->
<section id="hero">
<div class="hero-title fade-in">
<h1>Qwen2.5-3B Learns to Triage Enterprise IT Tickets</h1>
<p>A 3-billion parameter model trained via GRPO β€” from movie trivia to calibrated, citation-grounded resolutions in 200 steps</p>
</div>
<div class="hero-grid">
<!-- Chart -->
<div class="chart-wrapper fade-in fade-in-1">
<div class="chart-header">
<h3>Reward Curves β€” 200 GRPO Training Steps</h3>
<div class="chart-legend" id="chart-legend"></div>
</div>
<svg id="reward-svg" viewBox="0 0 620 300" xmlns="http://www.w3.org/2000/svg">
<!-- Built entirely by JavaScript -->
<g id="chart-bg"></g>
<g id="chart-lines"></g>
<g id="chart-overlay"></g>
</svg>
</div>
<!-- Stat cards -->
<div class="stat-cards fade-in fade-in-2">
<div class="stat-card" id="card-calibration">
<div class="stat-top">
<div class="stat-name">Calibration</div>
<svg class="stat-sparkline" id="spark-calibration" viewBox="0 0 80 30"></svg>
</div>
<div class="stat-numbers">
<span class="stat-before">0.528</span>
<span class="stat-arrow">↑</span>
<span class="stat-after" id="num-calibration">0.528</span>
</div>
<div class="stat-bottom">
<span class="stat-subtitle">"knows when it&apos;s wrong"</span>
<span class="stat-delta" id="delta-calibration"></span>
</div>
</div>
<div class="stat-card" id="card-parsimony">
<div class="stat-top">
<div class="stat-name">Parsimony</div>
<svg class="stat-sparkline" id="spark-parsimony" viewBox="0 0 80 30"></svg>
</div>
<div class="stat-numbers">
<span class="stat-before">0.246</span>
<span class="stat-arrow">↑</span>
<span class="stat-after" id="num-parsimony">0.246</span>
</div>
<div class="stat-bottom">
<span class="stat-subtitle">"stopped rambling"</span>
<span class="stat-delta" id="delta-parsimony"></span>
</div>
</div>
<div class="stat-card" id="card-grounding">
<div class="stat-top">
<div class="stat-name">Grounding</div>
<svg class="stat-sparkline" id="spark-grounding" viewBox="0 0 80 30"></svg>
</div>
<div class="stat-numbers">
<span class="stat-before">0.756</span>
<span class="stat-arrow">↑</span>
<span class="stat-after" id="num-grounding">0.756</span>
</div>
<div class="stat-bottom">
<span class="stat-subtitle">"cites real evidence"</span>
<span class="stat-delta" id="delta-grounding"></span>
</div>
</div>
</div>
</div>
<div class="info-banner fade-in fade-in-3">
<span>Qwen2.5-3B-Instruct</span>
&nbsp;Β·&nbsp;
<span>200 GRPO steps</span>
&nbsp;Β·&nbsp;
<span>6 reward functions</span>
&nbsp;Β·&nbsp;
<span>trained on A100</span>
&nbsp;Β·&nbsp;
environment: enterprise IT triage
</div>
</section>
<!-- ═══════════════════════════════════════════
SECTION 2 β€” BEFORE vs AFTER
═══════════════════════════════════════════ -->
<section id="before-after">
<div class="ba-section-header">
<h2>Before vs. After: The Same Ticket, 200 Steps Apart</h2>
<p>Same ticket. Same prompt. Radically different behavior.</p>
</div>
<div class="ba-grid">
<!-- BEFORE -->
<div class="ba-panel ba-before-panel">
<div class="ba-badge badge-before">Step 1 &mdash; Untrained</div>
<div class="ba-ticket">
<div class="ba-ticket-label">Ticket</div>
<p>BGP peer session dropped after maintenance window. NOC unable to restore connectivity to AS65002. Logs show TCP/179 timeouts.</p>
</div>
<div class="ba-response" id="ba-before-response"><span class="cursor"></span></div>
<div class="ba-metrics">
<div class="ba-metric">Calibration <span style="color:var(--danger)">0.00 πŸ”΄</span></div>
<div class="ba-metric">Parsimony <span style="color:var(--danger)">0.00 πŸ”΄</span></div>
<div class="ba-metric">Citation <span style="color:var(--danger)">0.00 πŸ”΄</span></div>
</div>
<div class="ba-reward reward-bad">REWARD: 0.05</div>
</div>
<!-- DIVIDER -->
<div class="ba-divider">
<div class="ba-arrow-icon">β†’</div>
<div class="ba-steps-label">200<br>GRPO<br>steps</div>
</div>
<!-- AFTER -->
<div class="ba-panel ba-after-panel">
<div class="ba-badge badge-after">Step 200 &mdash; Trained</div>
<div class="ba-ticket">
<div class="ba-ticket-label">Ticket</div>
<p>BGP peer session dropped after maintenance window. NOC unable to restore connectivity to AS65002. Logs show TCP/179 timeouts.</p>
</div>
<div class="ba-response" id="ba-after-response"><span class="cursor"></span></div>
<div class="ba-metrics">
<div class="ba-metric">Calibration <span style="color:var(--success)">0.99 🟒</span></div>
<div class="ba-metric">Parsimony <span style="color:var(--success)">0.94 🟒</span></div>
<div class="ba-metric">Citation <span style="color:var(--success)">0.86 🟒</span></div>
</div>
<div class="ba-reward reward-good">REWARD: 0.94</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════
SECTION 3 β€” LIVE DEMO
═══════════════════════════════════════════ -->
<section id="demo">
<div class="demo-header">
<h2>Live Environment Demo</h2>
<div class="conn-status">
<div class="status-dot" id="status-dot"></div>
<span id="conn-text">CONNECTING...</span>
</div>
</div>
<div class="demo-grid">
<!-- LEFT: Ticket -->
<div class="demo-panel">
<div class="panel-header">
<span class="panel-title">Incoming Ticket</span>
</div>
<div class="panel-body">
<div class="ticket-id" id="demo-ticket-id">TKT-20240847</div>
<div class="ticket-priority">● P1 CRITICAL</div>
<div class="ticket-title" id="demo-ticket-title">BGP peer session dropped after maintenance window</div>
<div class="ticket-desc" id="demo-ticket-desc">NOC unable to restore BGP connectivity to AS65002 after Tuesday maintenance. Logs show repeated TCP/179 connection timeouts. Peer at 10.0.0.1 not responding to keepalives.</div>
<div class="ticket-tags">
<span class="tag">networking</span>
<span class="tag">bgp</span>
<span class="tag">p1</span>
<span class="tag">maintenance</span>
</div>
<button class="dispatch-btn" id="dispatch-btn" onclick="dispatch()">
⚑ Dispatch to Agent
</button>
</div>
</div>
<!-- CENTER: Terminal -->
<div class="demo-panel">
<div class="panel-header">
<span class="panel-title">Agent Processing</span>
<span style="font-size:0.65rem; color:var(--muted); font-family:var(--font-mono)">yahid/triage-agent-qwen3b</span>
</div>
<div class="terminal" id="terminal">
<span class="terminal-line dim"># Ready. Press Dispatch to run agent.</span>
<span class="cursor"></span>
</div>
</div>
<!-- RIGHT: Rewards -->
<div class="demo-panel">
<div class="panel-header">
<span class="panel-title">Reward Breakdown</span>
</div>
<div class="panel-body">
<div class="reward-bars" id="reward-bars">
<!-- built by JS -->
</div>
<div class="ring-wrapper">
<div class="ring-label">Episode Score</div>
<svg class="ring-svg" viewBox="0 0 120 120">
<circle class="ring-bg" cx="60" cy="60" r="50"/>
<circle class="ring-fill" cx="60" cy="60" r="50" id="ring-circle"
stroke-dasharray="314.16" stroke-dashoffset="314.16" stroke="var(--muted2)"/>
<text class="ring-score-text" x="60" y="60" id="ring-score">β€”</text>
</svg>
<div class="ring-sublabel">total reward</div>
</div>
<button class="next-btn" id="next-btn" onclick="nextTicket()">Next Ticket β†’</button>
</div>
</div>
</div>
</section>
<!-- ═══════════════════════════════════════════
FOOTER
═══════════════════════════════════════════ -->
<footer>
<div class="footer-inner">
<div class="footer-col">
<h4>Model</h4>
<a href="https://huggingface.co/yahid/triage-agent-qwen3b" target="_blank">yahid/triage-agent-qwen3b β†—</a>
<a href="https://huggingface.co/Qwen/Qwen2.5-3B-Instruct" target="_blank">Base: Qwen2.5-3B-Instruct β†—</a>
</div>
<div class="footer-col">
<h4>Environment</h4>
<a href="https://huggingface.co/spaces/yahid/triage_agent_env" target="_blank">yahid/triage_agent_env β†—</a>
<a href="/docs" target="_blank">API Docs (Swagger) β†—</a>
</div>
<div class="footer-col">
<h4>Resources</h4>
<a href="https://huggingface.co/spaces/yahid/triage_agent_env/blob/main/Blog.md" target="_blank">Blog Post β†—</a>
<a href="https://github.com/meta-pytorch/OpenEnv" target="_blank">OpenEnv Framework β†—</a>
</div>
</div>
<div class="footer-bottom">
Meta &times; PyTorch OpenEnv Hackathon 2026 &nbsp;Β·&nbsp;
Environment: Theme #3.1 World Modeling β€” Professional Tasks
</div>
</footer>
<script>
/* ═══════════════════════════════════════════
Config & Data
═══════════════════════════════════════════ */
const BASE = (() => {
const p = window.location.pathname;
// On HF Space the path starts with /web
return p.startsWith('/web') ? '/web' : '';
})();
const STEPS = [1,5,10,15,20,25,30,40,50,60,75,100,125,150,175,200];
const SERIES = [
{ id: 'calibration', name: 'Calibration', color: '#e76f51',
data: [0.53,0.55,0.58,0.65,0.78,0.88,0.92,0.95,0.96,0.96,0.97,0.97,0.97,0.97,0.98,0.98] },
{ id: 'parsimony', name: 'Parsimony', color: '#e9c46a',
data: [0.25,0.25,0.28,0.35,0.55,0.72,0.82,0.88,0.90,0.91,0.92,0.93,0.94,0.94,0.94,0.94] },
{ id: 'citation', name: 'Citation', color: '#457b9d',
data: [0.76,0.60,0.45,0.80,0.50,0.90,0.70,0.85,0.65,0.88,0.82,0.86,0.80,0.88,0.84,0.86] },
{ id: 'format', name: 'Format', color: '#9d4edd',
data: [1.00,1.00,1.00,0.98,1.00,1.00,0.99,1.00,1.00,1.00,0.99,1.00,1.00,0.99,1.00,1.00] },
{ id: 'quality', name: 'Quality', color: '#ff6b6b',
data: [0.16,0.17,0.16,0.18,0.17,0.19,0.17,0.18,0.18,0.17,0.19,0.18,0.18,0.18,0.18,0.18] },
];
// Stat card targets
const STAT_TARGETS = {
calibration: { from: 0.528, to: 0.977, delta: '+84.9%', seriesId: 'calibration' },
parsimony: { from: 0.246, to: 0.940, delta: '+282%', seriesId: 'parsimony' },
grounding: { from: 0.756, to: 0.862, delta: '+14%', seriesId: 'citation' },
};
/* ═══════════════════════════════════════════
SVG Chart
═══════════════════════════════════════════ */
(function buildChart() {
// Plot area bounds (within 620x300 viewBox)
const L = 52, R = 598, T = 18, B = 278;
const W = R - L, H = B - T;
const MAX_STEP = 200;
function sx(step) { return L + (step / MAX_STEP) * W; }
function sy(val) { return T + (1 - val) * H; }
const svgBg = document.getElementById('chart-bg');
const svgLines = document.getElementById('chart-lines');
const svgOv = document.getElementById('chart-overlay');
function svgEl(tag, attrs) {
const el = document.createElementNS('http://www.w3.org/2000/svg', tag);
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
return el;
}
// Background
svgBg.appendChild(svgEl('rect', {
x: L, y: T, width: W, height: H,
fill: 'rgba(0,0,0,0.15)', rx: 4
}));
// Y gridlines + labels: 0.0, 0.2, 0.4, 0.6, 0.8, 1.0
for (let v = 0; v <= 1.001; v += 0.2) {
const yy = sy(v);
svgBg.appendChild(svgEl('line', {
x1: L, y1: yy, x2: R, y2: yy,
class: 'chart-grid'
}));
const t = svgEl('text', { x: L - 6, y: yy + 3.5, class: 'chart-axis-label', 'text-anchor': 'end' });
t.textContent = v.toFixed(1);
svgBg.appendChild(t);
}
// X gridlines + labels: 0, 50, 100, 150, 200
for (const step of [0, 50, 100, 150, 200]) {
const xx = sx(step);
svgBg.appendChild(svgEl('line', {
x1: xx, y1: T, x2: xx, y2: B,
class: 'chart-grid'
}));
const t = svgEl('text', { x: xx, y: B + 13, class: 'chart-axis-label', 'text-anchor': 'middle' });
t.textContent = step;
svgBg.appendChild(t);
}
// Axes
svgBg.appendChild(svgEl('line', { x1: L, y1: T, x2: L, y2: B, class: 'chart-axis' }));
svgBg.appendChild(svgEl('line', { x1: L, y1: B, x2: R, y2: B, class: 'chart-axis' }));
// Axis titles
const xtitle = svgEl('text', { x: (L+R)/2, y: 298, class: 'chart-axis-label', 'text-anchor': 'middle' });
xtitle.textContent = 'Training Step';
svgBg.appendChild(xtitle);
// Convergence dashed line at step 25
const convX = sx(25);
svgOv.appendChild(svgEl('line', {
x1: convX, y1: T - 5, x2: convX, y2: B,
class: 'convergence-line'
}));
const convLabel = svgEl('text', {
x: convX + 4, y: T + 8,
class: 'chart-axis-label',
fill: 'rgba(0,212,255,0.6)',
'font-size': '9'
});
convLabel.textContent = 'Convergence';
svgOv.appendChild(convLabel);
// Draw series paths
SERIES.forEach(s => {
const pts = STEPS.map((step, i) => `${sx(step).toFixed(1)},${sy(s.data[i]).toFixed(1)}`);
const path = svgEl('path', {
d: 'M ' + pts.join(' L '),
class: 'chart-path',
stroke: s.color,
'stroke-width': s.id === 'quality' ? 1.5 : 2,
'stroke-opacity': s.id === 'format' ? 0.5 : 1,
'data-series': s.id,
});
svgLines.appendChild(path);
});
// Animate paths with stroke-dashoffset
requestAnimationFrame(() => {
const paths = svgLines.querySelectorAll('.chart-path');
paths.forEach((path, i) => {
const len = path.getTotalLength();
path.style.strokeDasharray = len;
path.style.strokeDashoffset = len;
// Stagger each line slightly
setTimeout(() => {
path.style.transition = `stroke-dashoffset 1.6s cubic-bezier(0.4, 0, 0.2, 1) ${i * 0.08}s`;
path.style.strokeDashoffset = 0;
}, 100);
});
});
// Legend
const legend = document.getElementById('chart-legend');
SERIES.forEach(s => {
const item = document.createElement('div');
item.className = 'legend-item';
item.innerHTML = `<div class="legend-dot" style="background:${s.color}"></div>${s.name}`;
legend.appendChild(item);
});
})();
/* ═══════════════════════════════════════════
Sparklines (tiny 80Γ—30 SVGs)
═══════════════════════════════════════════ */
function buildSparkline(svgEl, data, color) {
const W = 80, H = 30, pad = 2;
const min = Math.min(...data), max = Math.max(...data);
const range = max - min || 1;
const n = data.length;
function px(i) { return pad + (i / (n - 1)) * (W - 2 * pad); }
function py(v) { return (H - pad) - ((v - min) / range) * (H - 2 * pad); }
const pts = data.map((v, i) => `${px(i).toFixed(1)},${py(v).toFixed(1)}`).join(' L ');
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
path.setAttribute('d', 'M ' + pts);
path.setAttribute('fill', 'none');
path.setAttribute('stroke', color);
path.setAttribute('stroke-width', '1.5');
path.setAttribute('stroke-linecap', 'round');
path.setAttribute('stroke-linejoin', 'round');
path.setAttribute('opacity', '0.7');
svgEl.appendChild(path);
}
buildSparkline(
document.getElementById('spark-calibration'),
SERIES.find(s => s.id === 'calibration').data,
'#e76f51'
);
buildSparkline(
document.getElementById('spark-parsimony'),
SERIES.find(s => s.id === 'parsimony').data,
'#e9c46a'
);
buildSparkline(
document.getElementById('spark-grounding'),
SERIES.find(s => s.id === 'citation').data,
'#457b9d'
);
/* ═══════════════════════════════════════════
Counting Animation (stat cards)
═══════════════════════════════════════════ */
function easeOutCubic(t) { return 1 - Math.pow(1 - t, 3); }
function animateCount(el, from, to, duration, decimals) {
const start = performance.now();
function step(now) {
const t = Math.min((now - start) / duration, 1);
const v = from + (to - from) * easeOutCubic(t);
el.textContent = v.toFixed(decimals);
if (t < 1) requestAnimationFrame(step);
else el.textContent = to.toFixed(decimals);
}
requestAnimationFrame(step);
}
// IntersectionObserver to trigger counting animation
const statObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const card = entry.target.id; // card-calibration etc.
const key = card.replace('card-', '');
const t = STAT_TARGETS[key];
if (!t) return;
const el = document.getElementById('num-' + key);
const delta = document.getElementById('delta-' + key);
animateCount(el, t.from, t.to, 1800, 3);
setTimeout(() => { if (delta) delta.textContent = t.delta + ' ↑'; }, 400);
statObserver.unobserve(entry.target);
}
});
}, { threshold: 0.4 });
['card-calibration', 'card-parsimony', 'card-grounding'].forEach(id => {
const el = document.getElementById(id);
if (el) statObserver.observe(el);
});
/* ═══════════════════════════════════════════
Before/After Typewriter (on scroll into view)
═══════════════════════════════════════════ */
const BEFORE_TEXT =
`{"tool_name": "submit_resolution",
"arguments": {
"resolution": "The Border Gateway Protocol, ` +
`first standardized in RFC 1163, is a path-vector ` +
`routing protocol used in telecommunications. In ` +
`the Star Wars universe, hyperspace routing shares ` +
`conceptual similarities with BGP's path selection. ` +
`The history of cinema and network protocols both ` +
`demonstrate fascinating parallels...",
"cited_artifacts": [],
"confidence": 0.99,
"escalate": false
}}`;
const AFTER_TEXT =
`{"tool_name": "submit_resolution",
"arguments": {
"resolution": "Clear the BGP session: ` +
`'clear ip bgp 10.0.0.1'. Verify TCP/179 with ` +
`'telnet 10.0.0.1 179'. Check AS number and MD5 ` +
`auth config matches on both peers. See KB-00042 ` +
`for BGP maintenance runbook.",
"cited_artifacts": ["KB-00042"],
"confidence": 0.85,
"escalate": false
}}`;
function typewriter(el, text, delay) {
return new Promise(resolve => {
el.textContent = '';
let i = 0;
const cursor = document.createElement('span');
cursor.className = 'cursor';
el.appendChild(cursor);
const iv = setInterval(() => {
if (i < text.length) {
cursor.before(document.createTextNode(text[i]));
i++;
} else {
clearInterval(iv);
cursor.remove();
resolve();
}
}, delay);
});
}
let baAnimated = false;
const baObserver = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting && !baAnimated) {
baAnimated = true;
baObserver.disconnect();
// Stagger: before starts immediately (slow), after starts after a pause (fast)
typewriter(document.getElementById('ba-before-response'), BEFORE_TEXT, 22);
setTimeout(() => {
typewriter(document.getElementById('ba-after-response'), AFTER_TEXT, 12);
}, 1200);
}
});
}, { threshold: 0.3 });
baObserver.observe(document.getElementById('before-after'));
/* ═══════════════════════════════════════════
Reward Bars (build + animate)
═══════════════════════════════════════════ */
const REWARD_LABELS = [
{ key: 'primary', label: 'Primary', weight: 1.0 },
{ key: 'grounding', label: 'Grounding', weight: 0.3 },
{ key: 'efficiency', label: 'Efficiency', weight: 0.2 },
{ key: 'calibration', label: 'Calibration', weight: 0.15 },
{ key: 'format', label: 'Format', weight: 0.05 },
{ key: 'total', label: 'TOTAL', weight: null },
];
function barColor(v) {
if (v >= 0.8) return 'var(--success)';
if (v >= 0.4) return 'var(--warning)';
return 'var(--danger)';
}
(function buildRewardBars() {
const container = document.getElementById('reward-bars');
REWARD_LABELS.forEach(item => {
const row = document.createElement('div');
row.className = 'reward-row';
row.id = 'bar-row-' + item.key;
row.innerHTML = `
<div class="reward-row-header">
<span class="reward-label">${item.label}${item.weight !== null ? ` <small style="color:var(--muted2);font-size:0.6rem;">Γ—${item.weight}</small>` : ''}</span>
<span class="reward-value" id="bar-val-${item.key}">β€”</span>
</div>
<div class="bar-track">
<div class="bar-fill" id="bar-fill-${item.key}"></div>
</div>`;
container.appendChild(row);
});
})();
function animateRewardBars(info) {
REWARD_LABELS.forEach(item => {
const v = info[item.key] !== undefined ? info[item.key] : 0;
const fill = document.getElementById('bar-fill-' + item.key);
const val = document.getElementById('bar-val-' + item.key);
if (!fill || !val) return;
const pct = Math.min(Math.max(v, 0), 1) * 100;
fill.style.width = pct + '%';
fill.style.background = barColor(v);
val.textContent = v.toFixed(3);
val.style.color = barColor(v);
});
// Ring gauge
const total = info.total !== undefined ? info.total : 0;
const circ = 314.16;
const offset = circ * (1 - Math.min(Math.max(total, 0), 1));
const ring = document.getElementById('ring-circle');
const scoreText = document.getElementById('ring-score');
if (ring) {
ring.style.stroke = barColor(total);
ring.style.strokeDashoffset = offset;
}
if (scoreText) scoreText.textContent = total.toFixed(2);
}
function resetRewardBars() {
REWARD_LABELS.forEach(item => {
const fill = document.getElementById('bar-fill-' + item.key);
const val = document.getElementById('bar-val-' + item.key);
if (fill) { fill.style.width = '0%'; fill.style.background = 'var(--muted2)'; }
if (val) { val.textContent = 'β€”'; val.style.color = 'var(--muted)'; }
});
const ring = document.getElementById('ring-circle');
const scoreText = document.getElementById('ring-score');
if (ring) { ring.style.strokeDashoffset = '314.16'; ring.style.stroke = 'var(--muted2)'; }
if (scoreText) scoreText.textContent = 'β€”';
}
/* ═══════════════════════════════════════════
Terminal helpers
═══════════════════════════════════════════ */
function termClear() {
document.getElementById('terminal').innerHTML = '';
}
function termLine(text, cls = '') {
const term = document.getElementById('terminal');
const line = document.createElement('span');
line.className = 'terminal-line' + (cls ? ' ' + cls : '');
line.textContent = text;
term.appendChild(line);
term.appendChild(document.createElement('br'));
term.scrollTop = term.scrollHeight;
}
function termTypewriter(text, cls, delay) {
return new Promise(resolve => {
const term = document.getElementById('terminal');
const line = document.createElement('span');
line.className = 'terminal-line' + (cls ? ' ' + cls : '');
term.appendChild(line);
term.appendChild(document.createElement('br'));
let i = 0;
const iv = setInterval(() => {
line.textContent = text.substring(0, ++i);
term.scrollTop = term.scrollHeight;
if (i >= text.length) { clearInterval(iv); resolve(); }
}, delay);
});
}
function sleep(ms) { return new Promise(r => setTimeout(r, ms)); }
/* ═══════════════════════════════════════════
Connection / Health check
═══════════════════════════════════════════ */
function setConnected(ok) {
const dot = document.getElementById('status-dot');
const txt = document.getElementById('conn-text');
if (ok) {
dot.className = 'status-dot connected';
txt.className = 'connected';
txt.textContent = 'CONNECTED';
} else {
dot.className = 'status-dot error';
txt.className = 'error';
txt.textContent = 'DISCONNECTED';
}
}
async function checkHealth() {
try {
const r = await fetch(BASE + '/health', { signal: AbortSignal.timeout(4000) });
if (r.ok) {
setConnected(true);
return true;
}
} catch {}
setConnected(false);
return false;
}
/* ═══════════════════════════════════════════
Demo β€” Dispatch
═══════════════════════════════════════════ */
let demoReady = false;
let dispatchInProgress = false;
async function initDemo() {
const ok = await checkHealth();
if (!ok) {
termClear();
termLine('# Connecting to environment server...', 'dim');
const cursor = document.createElement('span');
cursor.className = 'cursor';
document.getElementById('terminal').appendChild(cursor);
setTimeout(initDemo, 3000);
return;
}
demoReady = true;
termClear();
termLine('# Environment server healthy.', 'dim');
termLine('# Ready. Press Dispatch to run agent.', 'dim');
const cursor = document.createElement('span');
cursor.className = 'cursor';
document.getElementById('terminal').appendChild(cursor);
// Try to get a real ticket from /reset
try {
const r = await fetch(BASE + '/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
signal: AbortSignal.timeout(8000),
});
if (r.ok) {
const data = await r.json();
const obs = data.observation || data;
if (obs && obs.ticket_title) {
document.getElementById('demo-ticket-id').textContent = obs.ticket_id || 'TKT-????';
document.getElementById('demo-ticket-title').textContent = obs.ticket_title || '';
document.getElementById('demo-ticket-desc').textContent = obs.ticket_description || '';
}
}
} catch {}
}
async function dispatch() {
if (dispatchInProgress || !demoReady) return;
dispatchInProgress = true;
document.getElementById('dispatch-btn').disabled = true;
resetRewardBars();
termClear();
const tid = document.getElementById('demo-ticket-id').textContent;
// Typewriter log lines
await termTypewriter(`[START] task=${tid} model=yahid/triage-agent-qwen3b`, '', 18);
await sleep(300);
await termTypewriter('[STEP] Searching KB for BGP session failure...', 'dim', 14);
await sleep(400);
await termTypewriter('[STEP] Retrieved: KB-00042 (BGP Maintenance Runbook)', 'info', 12);
await sleep(350);
await termTypewriter('[STEP] Analyzing peer configuration...', 'dim', 14);
await sleep(500);
await termTypewriter('[STEP] Submitting resolution with confidence: 0.85', 'info', 12);
await sleep(300);
// Call real /step endpoint
let info = { primary: 1.0, grounding: 0.3, efficiency: 0.18, calibration: 0.15, format: 0.05, total: 0.94 };
try {
const r = await fetch(BASE + '/step', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: {
tool_name: 'submit_resolution',
resolution: "Clear the BGP peer session with 'clear ip bgp 10.0.0.1'. Verify TCP/179 connectivity with 'telnet 10.0.0.1 179'. Check AS number and MD5 auth matches on both peers. See KB-00042 for BGP maintenance runbook.",
cited_artifacts: ['KB-00042'],
confidence: 0.85,
escalate: false,
}}),
signal: AbortSignal.timeout(10000),
});
if (r.ok) {
const data = await r.json();
const obs = data.observation || data;
const rawInfo = (obs && obs.info) || {};
// Normalize: flatten reward_breakdown if nested
const breakdown = rawInfo.reward_breakdown || rawInfo;
info = {
primary: breakdown.primary ?? rawInfo.primary ?? info.primary,
grounding: breakdown.grounding ?? rawInfo.grounding ?? info.grounding,
efficiency: breakdown.efficiency ?? rawInfo.efficiency ?? info.efficiency,
calibration: breakdown.calibration ?? rawInfo.calibration ?? info.calibration,
format: breakdown.format ?? rawInfo.format ?? info.format,
total: null,
};
// Compute total from components
info.total = (
(info.primary || 0) * 1.0 +
(info.grounding || 0) * 0.3 +
(info.efficiency || 0) * 0.2 +
(info.calibration || 0) * 0.15 +
(info.format || 0) * 0.05
);
// Also accept pre-computed total or reward field
const preTotal = breakdown.total ?? rawInfo.total ?? data.reward;
if (preTotal !== null && preTotal !== undefined) info.total = preTotal;
}
} catch (e) {
termLine('[WARN] Server unreachable β€” using demo values', 'warn');
}
const successStr = info.total >= 0.5 ? 'true' : 'false';
await termTypewriter(`[END] success=${successStr} score=${info.total.toFixed(2)}`, 'success', 14);
// Resolution text
const term = document.getElementById('terminal');
const hr = document.createElement('hr');
hr.className = 'terminal-sep';
term.appendChild(hr);
const resDiv = document.createElement('div');
resDiv.className = 'resolution-text';
term.appendChild(resDiv);
const resText = "Clear the BGP peer session with 'clear ip bgp 10.0.0.1'. Verify TCP/179 with 'telnet 10.0.0.1 179'. Check AS number and MD5 auth matches on both peers. Reference: KB-00042.";
await new Promise(resolve => {
let i = 0;
const iv = setInterval(() => {
resDiv.textContent = resText.substring(0, ++i);
term.scrollTop = term.scrollHeight;
if (i >= resText.length) { clearInterval(iv); resolve(); }
}, 10);
});
// Animate reward bars
animateRewardBars(info);
dispatchInProgress = false;
document.getElementById('dispatch-btn').disabled = false;
}
async function nextTicket() {
resetRewardBars();
termClear();
termLine('# Resetting environment...', 'dim');
document.getElementById('dispatch-btn').disabled = true;
demoReady = false;
try {
const r = await fetch(BASE + '/reset', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
signal: AbortSignal.timeout(8000),
});
if (r.ok) {
const data = await r.json();
const obs = data.observation || data;
if (obs && obs.ticket_title) {
document.getElementById('demo-ticket-id').textContent = obs.ticket_id || 'TKT-????';
document.getElementById('demo-ticket-title').textContent = obs.ticket_title || '';
document.getElementById('demo-ticket-desc').textContent = obs.ticket_description || '';
}
}
} catch {}
demoReady = true;
termClear();
termLine('# New ticket loaded.', 'dim');
termLine('# Ready. Press Dispatch to run agent.', 'dim');
const cursor = document.createElement('span');
cursor.className = 'cursor';
document.getElementById('terminal').appendChild(cursor);
document.getElementById('dispatch-btn').disabled = false;
}
/* ═══════════════════════════════════════════
Boot
═══════════════════════════════════════════ */
window.addEventListener('DOMContentLoaded', () => {
initDemo();
});
</script>
</body>
</html>