Spaces:
Sleeping
Sleeping
| <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 — OpenEnv RL | |
| </div> | |
| <div class="nav-links"> | |
| <a href="#hero">Training Story</a> | |
| <a href="#before-after">Before & 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'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> | |
| Β· | |
| <span>200 GRPO steps</span> | |
| Β· | |
| <span>6 reward functions</span> | |
| Β· | |
| <span>trained on A100</span> | |
| Β· | |
| 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 — 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 — 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 × PyTorch OpenEnv Hackathon 2026 Β· | |
| 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> | |