Spaces:
Build error
Build error
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Stack Doctor — Incident War Room</title> | |
| <link rel="preconnect" href="https://fonts.googleapis.com"> | |
| <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@300;400;500;600&family=Outfit:wght@300;400;500;600;700&display=swap" rel="stylesheet"> | |
| <style> | |
| *, *::before, *::after { margin: 0; padding: 0; box-sizing: border-box; } | |
| :root { | |
| --bg-abyss: #060a11; | |
| --bg-deep: #0a0f1a; | |
| --bg-mid: #0f1623; | |
| --bg-surface: #151d2e; | |
| --bg-elevated: #1a2438; | |
| --border-subtle: rgba(100, 180, 255, 0.08); | |
| --border-active: rgba(0, 196, 255, 0.25); | |
| --cyan: #00c4ff; | |
| --cyan-bright: #40d4ff; | |
| --cyan-dim: rgba(0, 196, 255, 0.15); | |
| --cyan-glow: rgba(0, 196, 255, 0.4); | |
| --amber: #f0a030; | |
| --amber-dim: rgba(240, 160, 48, 0.15); | |
| --amber-glow: rgba(240, 160, 48, 0.4); | |
| --emerald: #00e676; | |
| --emerald-dim: rgba(0, 230, 118, 0.12); | |
| --emerald-glow: rgba(0, 230, 118, 0.35); | |
| --coral: #ff3d5a; | |
| --coral-dim: rgba(255, 61, 90, 0.12); | |
| --text-primary: #d8e0ec; | |
| --text-secondary: rgba(216, 224, 236, 0.55); | |
| --text-tertiary: rgba(216, 224, 236, 0.3); | |
| --font-display: 'Outfit', system-ui, sans-serif; | |
| --font-mono: 'IBM Plex Mono', 'SF Mono', monospace; | |
| --ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1); | |
| --duration-slow: 800ms; | |
| --duration-med: 400ms; | |
| --duration-fast: 200ms; | |
| } | |
| html { height: 100%; } | |
| body { | |
| min-height: 100%; | |
| background: var(--bg-abyss); | |
| color: var(--text-primary); | |
| font-family: var(--font-display); | |
| -webkit-font-smoothing: antialiased; | |
| overflow-y: auto; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; inset: 0; | |
| background: | |
| radial-gradient(ellipse 80% 60% at 20% 80%, rgba(0, 100, 180, 0.06) 0%, transparent 70%), | |
| radial-gradient(ellipse 60% 50% at 80% 20%, rgba(0, 160, 255, 0.04) 0%, transparent 60%); | |
| pointer-events: none; z-index: 0; | |
| } | |
| .grid-overlay { | |
| position: fixed; inset: 0; z-index: 0; pointer-events: none; | |
| background-image: | |
| linear-gradient(rgba(100, 180, 255, 0.015) 1px, transparent 1px), | |
| linear-gradient(90deg, rgba(100, 180, 255, 0.015) 1px, transparent 1px); | |
| background-size: 60px 60px; | |
| } | |
| .app { | |
| position: relative; z-index: 1; | |
| max-width: 1400px; | |
| margin: 0 auto; | |
| padding: 20px 24px 40px; | |
| display: flex; flex-direction: column; gap: 16px; | |
| } | |
| /* ══════════ HEADER ══════════ */ | |
| .header { | |
| display: flex; align-items: center; justify-content: space-between; | |
| padding: 14px 24px; | |
| background: linear-gradient(135deg, var(--bg-mid), var(--bg-surface)); | |
| border: 1px solid var(--border-subtle); border-radius: 14px; | |
| } | |
| .header-left { display: flex; align-items: center; gap: 14px; } | |
| .logo-mark { | |
| width: 36px; height: 36px; border-radius: 9px; | |
| background: linear-gradient(135deg, var(--cyan), rgba(0, 196, 255, 0.5)); | |
| display: flex; align-items: center; justify-content: center; | |
| box-shadow: 0 0 24px var(--cyan-dim); | |
| animation: logoPulse 4s ease-in-out infinite; | |
| } | |
| .logo-mark svg { width: 20px; height: 20px; } | |
| @keyframes logoPulse { 0%,100% { box-shadow: 0 0 20px var(--cyan-dim); } 50% { box-shadow: 0 0 36px var(--cyan-glow); } } | |
| .header-title { font-weight: 600; font-size: 16px; letter-spacing: 0.5px; } | |
| .header-subtitle { font-family: var(--font-mono); font-size: 11px; font-weight: 300; color: var(--text-secondary); } | |
| .header-right { display: flex; align-items: center; gap: 20px; } | |
| .header-meta-label { font-family: var(--font-mono); font-size: 9px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-tertiary); } | |
| .header-meta-value { font-family: var(--font-mono); font-size: 12px; color: var(--cyan); } | |
| .status-badge { | |
| font-family: var(--font-mono); font-size: 10px; font-weight: 500; | |
| letter-spacing: 1px; text-transform: uppercase; | |
| padding: 5px 14px; border-radius: 20px; | |
| background: var(--cyan-dim); color: var(--cyan); | |
| border: 1px solid rgba(0, 196, 255, 0.2); | |
| transition: all var(--duration-slow) var(--ease-out-expo); | |
| } | |
| .status-badge.warning { background: var(--amber-dim); color: var(--amber); border-color: rgba(240, 160, 48, 0.3); } | |
| .status-badge.success { background: var(--emerald-dim); color: var(--emerald); border-color: rgba(0, 230, 118, 0.3); } | |
| .status-badge.error { background: var(--coral-dim); color: var(--coral); border-color: rgba(255, 61, 90, 0.3); } | |
| /* ══════════ SECTION TITLES ══════════ */ | |
| .section-title { | |
| font-family: var(--font-mono); font-size: 10px; font-weight: 500; | |
| letter-spacing: 2px; text-transform: uppercase; color: var(--text-tertiary); | |
| padding: 8px 0 0; | |
| display: flex; align-items: center; gap: 10px; | |
| } | |
| .section-title::after { content: ''; flex: 1; height: 1px; background: var(--border-subtle); } | |
| /* ══════════ TRAINING CHART ══════════ */ | |
| .chart-section { | |
| display: grid; grid-template-columns: 1fr 1fr; gap: 16px; | |
| } | |
| .chart-panel { | |
| background: linear-gradient(180deg, rgba(15, 22, 35, 0.85), rgba(10, 15, 26, 0.95)); | |
| border: 1px solid var(--border-subtle); border-radius: 14px; | |
| padding: 24px 28px; position: relative; overflow: hidden; | |
| } | |
| .chart-panel-title { | |
| font-family: var(--font-mono); font-size: 10px; font-weight: 500; | |
| letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-secondary); | |
| margin-bottom: 4px; | |
| } | |
| .chart-panel-subtitle { | |
| font-size: 13px; font-weight: 300; color: var(--text-tertiary); | |
| margin-bottom: 16px; | |
| } | |
| .chart-canvas-wrap { | |
| position: relative; width: 100%; height: 280px; | |
| } | |
| canvas { width: 100% ; height: 100% ; } | |
| .chart-stat-row { | |
| display: flex; gap: 20px; margin-top: 16px; padding-top: 14px; | |
| border-top: 1px solid var(--border-subtle); | |
| } | |
| .chart-stat { display: flex; flex-direction: column; gap: 2px; } | |
| .chart-stat-label { font-family: var(--font-mono); font-size: 9px; font-weight: 500; letter-spacing: 1px; text-transform: uppercase; color: var(--text-tertiary); } | |
| .chart-stat-value { font-family: var(--font-mono); font-size: 18px; font-weight: 300; } | |
| .chart-stat-value.emerald { color: var(--emerald); } | |
| .chart-stat-value.coral { color: var(--coral); } | |
| .chart-stat-value.cyan { color: var(--cyan); } | |
| .chart-stat-value.amber { color: var(--amber); } | |
| /* ══════════ ANNOTATION BADGES ══════════ */ | |
| .annotation { | |
| position: absolute; font-family: var(--font-mono); font-size: 9px; | |
| font-weight: 500; letter-spacing: 0.5px; padding: 3px 8px; | |
| border-radius: 4px; pointer-events: none; white-space: nowrap; | |
| } | |
| /* ══════════ WAR ROOM GRID ══════════ */ | |
| .warroom { display: grid; grid-template-columns: 220px 1fr 260px; gap: 14px; } | |
| .panel { | |
| background: linear-gradient(180deg, rgba(15, 22, 35, 0.85), rgba(10, 15, 26, 0.95)); | |
| border: 1px solid var(--border-subtle); border-radius: 14px; overflow: hidden; | |
| } | |
| .panel-header { | |
| padding: 12px 16px 8px; display: flex; align-items: center; gap: 8px; | |
| border-bottom: 1px solid var(--border-subtle); | |
| } | |
| .panel-header-dot { | |
| width: 6px; height: 6px; border-radius: 50%; | |
| background: var(--cyan); box-shadow: 0 0 8px var(--cyan-glow); | |
| animation: dotPulse 3s ease-in-out infinite; | |
| } | |
| @keyframes dotPulse { 0%,100% { opacity: 0.6; } 50% { opacity: 1; } } | |
| .panel-header-title { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-secondary); } | |
| /* ══════════ ARCHITECTURE DIAGRAM ══════════ */ | |
| .arch-body { | |
| display: flex; flex-direction: column; align-items: center; | |
| padding: 16px 14px; gap: 0; | |
| } | |
| .arch-layer { | |
| width: 100%; padding: 12px; | |
| background: var(--bg-deep); border: 1px solid var(--border-subtle); border-radius: 8px; | |
| transition: all var(--duration-slow) var(--ease-out-expo); | |
| } | |
| .arch-layer .layer-name { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-secondary); transition: color var(--duration-slow); } | |
| .arch-layer .layer-detail { font-family: var(--font-mono); font-size: 9px; font-weight: 300; color: var(--text-tertiary); margin-top: 2px; } | |
| .arch-layer.scanning { border-color: rgba(0, 196, 255, 0.3); background: linear-gradient(135deg, rgba(0, 196, 255, 0.06), var(--bg-deep)); box-shadow: 0 0 20px var(--cyan-dim); } | |
| .arch-layer.scanning .layer-name { color: var(--cyan); } | |
| .arch-layer.identified { border-color: rgba(240, 160, 48, 0.4); background: linear-gradient(135deg, rgba(240, 160, 48, 0.08), var(--bg-deep)); box-shadow: 0 0 25px var(--amber-dim); animation: identPulse 2s ease-in-out infinite; } | |
| .arch-layer.identified .layer-name { color: var(--amber); } | |
| @keyframes identPulse { 0%,100% { box-shadow: 0 0 20px var(--amber-dim); } 50% { box-shadow: 0 0 35px rgba(240, 160, 48, 0.25); } } | |
| .arch-layer.resolved { border-color: rgba(0, 230, 118, 0.3); background: linear-gradient(135deg, rgba(0, 230, 118, 0.06), var(--bg-deep)); box-shadow: 0 0 20px var(--emerald-dim); } | |
| .arch-layer.resolved .layer-name { color: var(--emerald); } | |
| .arch-connector { width: 1px; height: 10px; background: linear-gradient(180deg, var(--border-active), transparent); position: relative; } | |
| .arch-connector .data-dot { width: 3px; height: 3px; border-radius: 50%; background: var(--cyan); position: absolute; left: -1px; animation: flowDown 2s linear infinite; opacity: 0.6; } | |
| @keyframes flowDown { 0% { top: 0; opacity: 0; } 20% { opacity: 0.8; } 80% { opacity: 0.8; } 100% { top: 100%; opacity: 0; } } | |
| /* ══════════ INVESTIGATION LOG ══════════ */ | |
| .log-body { | |
| height: 380px; overflow-y: auto; padding: 12px 16px; | |
| display: flex; flex-direction: column; gap: 8px; | |
| scrollbar-width: thin; scrollbar-color: rgba(100, 180, 255, 0.1) transparent; | |
| } | |
| .idle-prompt { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 100%; gap: 12px; padding: 40px; } | |
| .idle-prompt .idle-text { font-size: 13px; color: var(--text-secondary); text-align: center; line-height: 1.6; } | |
| .incident-card { | |
| background: linear-gradient(135deg, rgba(0, 196, 255, 0.04), var(--bg-surface)); | |
| border: 1px solid rgba(0, 196, 255, 0.12); border-radius: 10px; | |
| padding: 14px; animation: cardIn 0.6s var(--ease-out-expo) both; | |
| } | |
| @keyframes cardIn { from { opacity: 0; transform: translateY(8px); } } | |
| .incident-label { font-family: var(--font-mono); font-size: 9px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; color: var(--cyan); margin-bottom: 8px; } | |
| .incident-text { font-size: 12px; line-height: 1.6; color: var(--text-primary); } | |
| .incident-meta { display: flex; gap: 16px; margin-top: 10px; flex-wrap: wrap; } | |
| .incident-meta-item .meta-label { font-family: var(--font-mono); font-size: 8px; font-weight: 500; letter-spacing: 1.2px; text-transform: uppercase; color: var(--text-tertiary); } | |
| .incident-meta-item .meta-value { font-family: var(--font-mono); font-size: 11px; color: var(--text-secondary); } | |
| .log-entry { | |
| display: flex; gap: 10px; padding: 10px 12px; | |
| background: var(--bg-deep); border: 1px solid var(--border-subtle); border-radius: 8px; | |
| animation: entryIn 0.5s var(--ease-out-expo) both; | |
| } | |
| @keyframes entryIn { from { opacity: 0; transform: translateX(-12px); } } | |
| .log-entry-icon { | |
| width: 24px; height: 24px; border-radius: 6px; | |
| display: flex; align-items: center; justify-content: center; | |
| flex-shrink: 0; font-size: 11px; | |
| } | |
| .log-entry-icon.inspect { background: var(--cyan-dim); color: var(--cyan); } | |
| .log-entry-icon.specialist { background: rgba(160, 120, 255, 0.12); color: #a078ff; } | |
| .log-entry-icon.fix { background: var(--amber-dim); color: var(--amber); } | |
| .log-entry-icon.submit { background: var(--emerald-dim); color: var(--emerald); } | |
| .log-entry-content { flex: 1; min-width: 0; } | |
| .log-entry-header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 3px; } | |
| .log-entry-type { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 0.8px; text-transform: uppercase; } | |
| .log-entry-type.cyan { color: var(--cyan); } | |
| .log-entry-type.purple { color: #a078ff; } | |
| .log-entry-type.amber { color: var(--amber); } | |
| .log-entry-type.emerald { color: var(--emerald); } | |
| .log-entry-step { font-family: var(--font-mono); font-size: 9px; color: var(--text-tertiary); } | |
| .log-entry-text { font-family: var(--font-mono); font-size: 10px; font-weight: 300; line-height: 1.55; color: var(--text-secondary); white-space: pre-wrap; word-break: break-word; } | |
| .log-entry-reward { font-family: var(--font-mono); font-size: 10px; font-weight: 500; margin-top: 4px; } | |
| .log-entry-reward.positive { color: var(--emerald); } | |
| .log-entry-reward.negative { color: var(--coral); } | |
| /* ══════════ SPECIALISTS ══════════ */ | |
| .specialists-body { padding: 10px 12px; display: flex; flex-direction: column; gap: 8px; overflow-y: auto; max-height: 420px; } | |
| .specialist-card { | |
| background: var(--bg-deep); border: 1px solid var(--border-subtle); border-radius: 10px; | |
| padding: 10px 12px; transition: all var(--duration-slow) var(--ease-out-expo); | |
| animation: cardIn 0.5s var(--ease-out-expo) both; | |
| } | |
| .specialist-card.highlighted { border-color: rgba(160, 120, 255, 0.3); background: linear-gradient(135deg, rgba(160, 120, 255, 0.05), var(--bg-deep)); } | |
| .specialist-card.wrong { border-color: rgba(255, 61, 90, 0.15); opacity: 0.5; } | |
| .specialist-card.correct { border-color: rgba(0, 230, 118, 0.2); } | |
| .specialist-top { display: flex; align-items: center; justify-content: space-between; margin-bottom: 6px; } | |
| .specialist-name { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 1px; text-transform: uppercase; color: var(--text-secondary); transition: color var(--duration-med); } | |
| .specialist-card.highlighted .specialist-name { color: #a078ff; } | |
| .specialist-card.correct .specialist-name { color: var(--emerald); } | |
| .specialist-card.wrong .specialist-name { color: var(--coral); } | |
| .confidence-bar { width: 50px; height: 3px; background: rgba(255,255,255,0.06); border-radius: 2px; overflow: hidden; } | |
| .confidence-fill { height: 100%; border-radius: 2px; background: var(--cyan); transition: width 1s var(--ease-out-expo); } | |
| .specialist-card.wrong .confidence-fill { background: var(--coral); } | |
| .specialist-card.correct .confidence-fill { background: var(--emerald); } | |
| .specialist-opinion { font-size: 11px; font-weight: 300; line-height: 1.4; color: var(--text-secondary); } | |
| .specialist-card.wrong .specialist-opinion { opacity: 0.5; } | |
| .specialist-verdict { font-family: var(--font-mono); font-size: 9px; font-weight: 500; letter-spacing: 1px; text-transform: uppercase; margin-top: 6px; opacity: 0; transition: opacity var(--duration-med); } | |
| .specialist-card.wrong .specialist-verdict, .specialist-card.correct .specialist-verdict { opacity: 1; } | |
| .specialist-card.wrong .specialist-verdict { color: var(--coral); } | |
| .specialist-card.correct .specialist-verdict { color: var(--emerald); } | |
| /* ══════════ VITALS BAR ══════════ */ | |
| .vitals-bar { | |
| display: grid; grid-template-columns: repeat(5, 1fr); gap: 10px; | |
| } | |
| .vital { | |
| background: linear-gradient(135deg, var(--bg-mid), var(--bg-surface)); | |
| border: 1px solid var(--border-subtle); border-radius: 10px; | |
| padding: 10px 14px; | |
| } | |
| .vital-label { font-family: var(--font-mono); font-size: 9px; font-weight: 500; letter-spacing: 1.5px; text-transform: uppercase; color: var(--text-tertiary); margin-bottom: 4px; } | |
| .vital-value { font-family: var(--font-mono); font-size: 20px; font-weight: 300; transition: color var(--duration-med); } | |
| .vital-value.cyan { color: var(--cyan); } | |
| .vital-value.amber { color: var(--amber); } | |
| .vital-value.emerald { color: var(--emerald); } | |
| .vital-value.coral { color: var(--coral); } | |
| .steps-dots { display: flex; gap: 5px; margin-top: 4px; } | |
| .step-dot { width: 8px; height: 8px; border-radius: 50%; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.06); transition: all var(--duration-med); } | |
| .step-dot.used { background: var(--cyan); border-color: var(--cyan); box-shadow: 0 0 8px var(--cyan-dim); } | |
| .step-dot.current { background: var(--amber); border-color: var(--amber); box-shadow: 0 0 8px var(--amber-dim); animation: dotPulse 1.5s ease-in-out infinite; } | |
| /* ══════════ CONTROLS ══════════ */ | |
| .controls { | |
| display: flex; justify-content: center; gap: 12px; padding: 4px 0; | |
| } | |
| .ctrl-btn { | |
| font-family: var(--font-mono); font-size: 12px; font-weight: 500; | |
| letter-spacing: 1px; text-transform: uppercase; | |
| padding: 12px 28px; border-radius: 10px; | |
| cursor: pointer; transition: all var(--duration-fast) ease; | |
| border: none; | |
| } | |
| .ctrl-btn.primary { | |
| background: linear-gradient(135deg, var(--cyan), rgba(0, 160, 220, 0.9)); | |
| color: #fff; box-shadow: 0 0 24px var(--cyan-dim); | |
| } | |
| .ctrl-btn.primary:hover { box-shadow: 0 0 40px var(--cyan-glow); transform: translateY(-1px); } | |
| .ctrl-btn.primary:disabled { opacity: 0.3; cursor: not-allowed; transform: none; box-shadow: none; } | |
| .ctrl-btn.secondary { | |
| background: var(--bg-surface); color: var(--text-secondary); | |
| border: 1px solid var(--border-subtle); | |
| } | |
| .ctrl-btn.secondary:hover { border-color: var(--border-active); color: var(--text-primary); } | |
| .server-input { | |
| font-family: var(--font-mono); font-size: 11px; padding: 10px 14px; | |
| background: var(--bg-deep); color: var(--cyan); border: 1px solid var(--border-subtle); | |
| border-radius: 8px; width: 200px; outline: none; | |
| } | |
| .server-input:focus { border-color: var(--border-active); } | |
| .conn-status { | |
| font-family: var(--font-mono); font-size: 10px; text-transform: uppercase; | |
| letter-spacing: 1px; color: var(--text-tertiary); padding: 0 8px; | |
| } | |
| .conn-status.connected { color: var(--emerald); } | |
| .conn-status.error { color: var(--coral); } | |
| .conn-status.running { color: var(--amber); } | |
| /* ══════════ DIAGNOSIS OVERLAY ══════════ */ | |
| .diagnosis-overlay { | |
| position: fixed; inset: 0; | |
| background: rgba(6, 10, 17, 0.85); backdrop-filter: blur(20px); | |
| display: flex; align-items: center; justify-content: center; | |
| z-index: 100; opacity: 0; pointer-events: none; | |
| transition: opacity 0.6s var(--ease-out-expo); | |
| } | |
| .diagnosis-overlay.visible { opacity: 1; pointer-events: auto; } | |
| .diagnosis-card { | |
| background: linear-gradient(180deg, var(--bg-surface), var(--bg-deep)); | |
| border: 1px solid var(--border-active); border-radius: 20px; | |
| padding: 40px 48px; max-width: 520px; width: 100%; text-align: center; | |
| transform: scale(0.92) translateY(20px); | |
| transition: transform 0.8s var(--ease-out-expo); | |
| box-shadow: 0 0 60px rgba(0, 196, 255, 0.08), 0 20px 60px rgba(0, 0, 0, 0.4); | |
| } | |
| .diagnosis-overlay.visible .diagnosis-card { transform: scale(1) translateY(0); } | |
| .diagnosis-title { font-family: var(--font-mono); font-size: 11px; font-weight: 500; letter-spacing: 3px; text-transform: uppercase; color: var(--cyan); margin-bottom: 24px; } | |
| .diagnosis-result { display: flex; flex-direction: column; gap: 12px; margin-bottom: 28px; } | |
| .diagnosis-row { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--bg-deep); border-radius: 10px; border: 1px solid var(--border-subtle); } | |
| .diagnosis-row-label { font-family: var(--font-mono); font-size: 10px; font-weight: 500; letter-spacing: 1px; text-transform: uppercase; color: var(--text-tertiary); } | |
| .diagnosis-row-value { font-family: var(--font-mono); font-size: 13px; } | |
| .diagnosis-row-value.correct { color: var(--emerald); } | |
| .diagnosis-row-value.wrong { color: var(--coral); } | |
| .diagnosis-reward { font-size: 48px; font-weight: 700; letter-spacing: -2px; margin-bottom: 8px; } | |
| .diagnosis-reward-label { font-family: var(--font-mono); font-size: 10px; letter-spacing: 1px; text-transform: uppercase; color: var(--text-tertiary); } | |
| ::-webkit-scrollbar { width: 4px; } | |
| ::-webkit-scrollbar-track { background: transparent; } | |
| ::-webkit-scrollbar-thumb { background: rgba(100, 180, 255, 0.15); border-radius: 4px; } | |
| @media (max-width: 1000px) { | |
| .warroom { grid-template-columns: 1fr; } | |
| .chart-section { grid-template-columns: 1fr; } | |
| .vitals-bar { grid-template-columns: repeat(3, 1fr); } | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="grid-overlay"></div> | |
| <div class="app"> | |
| <header class="header"> | |
| <div class="header-left"> | |
| <div class="logo-mark"> | |
| <svg viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round"> | |
| <path d="M12 3v18M3 12h18M7 7l10 10M17 7L7 17"/> | |
| </svg> | |
| </div> | |
| <div> | |
| <div class="header-title">Stack Doctor</div> | |
| <div class="header-subtitle">Incident War Room</div> | |
| </div> | |
| </div> | |
| <div class="header-right"> | |
| <div> | |
| <div class="header-meta-label">Scenario</div> | |
| <div class="header-meta-value" id="scenarioId">—</div> | |
| </div> | |
| <div> | |
| <div class="header-meta-label">Episode</div> | |
| <div class="header-meta-value" id="episodeTime">00:00</div> | |
| </div> | |
| <div class="status-badge" id="statusBadge">Standby</div> | |
| </div> | |
| </header> | |
| <!-- ══════════ TRAINING DATA ══════════ --> | |
| <div class="section-title">Training Analytics</div> | |
| <div class="chart-section" style="grid-template-columns: 1fr; max-width: 900px; margin: 0 auto; width: 100%;"> | |
| <div class="chart-panel"> | |
| <div class="chart-panel-title">Qwen3.5-9B — Episode Reward</div> | |
| <div class="chart-panel-subtitle">100 GRPO steps — base model already near-oracle</div> | |
| <div class="chart-canvas-wrap"><canvas id="rewardChart"></canvas></div> | |
| <div class="chart-stat-row"> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Peak</div> | |
| <div class="chart-stat-value emerald">+26.00</div> | |
| </div> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Base Avg</div> | |
| <div class="chart-stat-value cyan">+19.50</div> | |
| </div> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Zero-Std</div> | |
| <div class="chart-stat-value coral">72%</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chart-panel"> | |
| <div class="chart-panel-title">Qwen3.5-9B — Completion Length</div> | |
| <div class="chart-panel-subtitle">Thinking mode consumed token budget, hit 2048 cap</div> | |
| <div class="chart-canvas-wrap"><canvas id="lengthChart"></canvas></div> | |
| <div class="chart-stat-row"> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Collapse</div> | |
| <div class="chart-stat-value coral">Step 36</div> | |
| </div> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Clipping</div> | |
| <div class="chart-stat-value amber">Step 69</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chart-panel" style="border-color: rgba(0, 196, 255, 0.15);"> | |
| <div class="chart-panel-title">Qwen2.5-1.5B — Episode Reward</div> | |
| <div class="chart-panel-subtitle">16 GRPO steps — weak model, real gradient signal</div> | |
| <div class="chart-canvas-wrap"><canvas id="reward1bChart"></canvas></div> | |
| <div class="chart-stat-row"> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Best Step</div> | |
| <div class="chart-stat-value cyan">-1.75</div> | |
| </div> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Avg</div> | |
| <div class="chart-stat-value amber">-4.90</div> | |
| </div> | |
| <div class="chart-stat"> | |
| <div class="chart-stat-label">Zero-Std</div> | |
| <div class="chart-stat-value emerald">0%</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- ══════════ DEMO CONTROLS ══════════ --> | |
| <div class="section-title">Live Environment</div> | |
| <div class="controls"> | |
| <input type="text" id="serverUrl" class="server-input" placeholder="Server URL (empty = same origin)"> | |
| <script> | |
| /* Auto-detect: use localhost:8000 for local dev, empty for HF Spaces */ | |
| if (location.hostname === 'localhost' || location.hostname === '127.0.0.1') { | |
| document.getElementById('serverUrl').value = 'http://localhost:8000'; | |
| } | |
| </script> | |
| <button class="ctrl-btn primary" id="demoBtn" onclick="runComparison()">▶ Run Comparison (Base → GRPO Trained)</button> | |
| <button class="ctrl-btn secondary" id="resetBtn" onclick="resetState()">↺ Reset</button> | |
| <span id="modelStatus" class="conn-status" style="margin-left:8px;">Model: checking...</span> | |
| <span id="connStatus" class="conn-status">Disconnected</span> | |
| </div> | |
| <!-- ══════════ WAR ROOM ══════════ --> | |
| <div class="warroom"> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <div class="panel-header-dot"></div> | |
| <div class="panel-header-title">Inference Stack</div> | |
| </div> | |
| <div class="arch-body" id="archBody"> | |
| <div class="arch-layer" id="layer-model"><div class="layer-name">Model</div><div class="layer-detail" id="detail-model">—</div></div> | |
| <div class="arch-connector"><div class="data-dot"></div></div> | |
| <div class="arch-layer" id="layer-kernel"><div class="layer-name">Kernel</div><div class="layer-detail">Attention / GEMM</div></div> | |
| <div class="arch-connector"><div class="data-dot" style="animation-delay:-0.5s"></div></div> | |
| <div class="arch-layer" id="layer-backend"><div class="layer-name">Backend</div><div class="layer-detail" id="detail-backend">—</div></div> | |
| <div class="arch-connector"><div class="data-dot" style="animation-delay:-1s"></div></div> | |
| <div class="arch-layer" id="layer-runtime"><div class="layer-name">Runtime</div><div class="layer-detail">CUDA / ROCm</div></div> | |
| <div class="arch-connector"><div class="data-dot" style="animation-delay:-1.5s"></div></div> | |
| <div class="arch-layer" id="layer-memory"><div class="layer-name">Memory</div><div class="layer-detail">HBM / KV Cache</div></div> | |
| <div class="arch-connector"><div class="data-dot" style="animation-delay:-2s"></div></div> | |
| <div class="arch-layer" id="layer-driver"><div class="layer-name">Driver</div><div class="layer-detail" id="detail-driver">—</div></div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <div class="panel-header-dot"></div> | |
| <div class="panel-header-title">Investigation Log</div> | |
| </div> | |
| <div class="log-body" id="logBody"> | |
| <div class="idle-prompt" id="idlePrompt"> | |
| <div class="idle-text">Awaiting incident assignment.<br>Click <strong>Run Demo</strong> above to start.</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="panel"> | |
| <div class="panel-header"> | |
| <div class="panel-header-dot"></div> | |
| <div class="panel-header-title">Specialist Agents</div> | |
| </div> | |
| <div class="specialists-body" id="specialistsBody"></div> | |
| </div> | |
| </div> | |
| <!-- ══════════ VITALS ══════════ --> | |
| <div class="vitals-bar"> | |
| <div class="vital"> | |
| <div class="vital-label">Steps</div> | |
| <div class="steps-dots" id="stepsDots"> | |
| <div class="step-dot"></div><div class="step-dot"></div><div class="step-dot"></div> | |
| <div class="step-dot"></div><div class="step-dot"></div><div class="step-dot"></div> | |
| </div> | |
| </div> | |
| <div class="vital"><div class="vital-label">Reward</div><div class="vital-value" id="rewardValue">0.00</div></div> | |
| <div class="vital"><div class="vital-label">Fix Status</div><div class="vital-value" id="fixStatus" style="font-size:13px">Not Applied</div></div> | |
| <div class="vital"><div class="vital-label">Root Cause</div><div class="vital-value" id="rootCauseValue" style="font-size:13px">—</div></div> | |
| <div class="vital"><div class="vital-label">Diagnosis</div><div class="vital-value" id="diagnosisValue" style="font-size:13px">Pending</div></div> | |
| </div> | |
| </div> | |
| <div class="diagnosis-overlay" id="diagnosisOverlay"> | |
| <div class="diagnosis-card"> | |
| <div class="diagnosis-title">Diagnosis Submitted</div> | |
| <div class="diagnosis-result" id="diagnosisResult"></div> | |
| <div class="diagnosis-reward" id="diagnosisReward">+0.00</div> | |
| <div class="diagnosis-reward-label">Episode Reward</div> | |
| </div> | |
| </div> | |
| <script> | |
| /* ═══════════════════════════════════════════════ | |
| TRAINING DATA — Qwen3.5-9B, 100 GRPO steps | |
| ═══════════════════════════════════════════════ */ | |
| var TRAIN_DATA = { | |
| steps: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100], | |
| reward: [9.65,19.75,7.25,24.25,23.00,22.75,2.62,22.75,23.00,24.50,16.00,23.00,24.50,7.25,24.62,26.00,26.00,26.00,22.50,14.12,-5.45,7.12,-2.60,-8.50,18.50,0.88,26.25,7.88,9.62,7.88,6.88,24.25,-6.25,-5.50,-1.88,-1.75,-1.75,-5.12,-2.62,-1.75,-1.75,-1.75,-2.62,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-5.12,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-1.75,-5.12,-5.12,-1.75,-1.75,-1.75,-1.75,-8.50,-5.12,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-8.50,-5.00,-8.50,-8.50,-8.50,-1.75,-5.12,-1.75,-1.75,-1.75,-5.12,-1.75,-1.75,-0.12,-1.75,-2.62,-1.75,-1.75], | |
| completion_length: [68,98,91,96,85,92,96,74,99,71,86,116,112,124,126,175,105,120,152,112,148,190,193,182,164,135,140,218,130,152,164,182,134,93,24,15,18,87,16,16,15,15,15,15,13,15,15,16,15,15,125,62,180,876,376,280,883,484,734,470,432,488,354,177,471,607,248,210,1234,3,2048,2048,2048,2048,2048,1024,1026,1078,2048,1025,14,1030,2048,1025,2048,2048,2048,1364,1032,15,15,15,14,15,15,1032,2048,1065,2048,1058], | |
| }; | |
| /* Qwen2.5-1.5B — 16 steps before crash (fixed in next run) */ | |
| var TRAIN_DATA_1B = { | |
| steps: [1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16], | |
| reward: [-5.625,-3.375,-8.375,-7.75,-2.75,-2.625,-3.0,-5.125,-8.875,-3.0,-1.75,-3.875,-9.5,-5.5,-3.125,-6.125], | |
| completion_length: [65.5,57.5,53,36.5,81,98,89,41.5,46,62.5,75,72.5,48,52.5,44,60], | |
| }; | |
| /* ═══════════════════════════════════════════════ | |
| CHARTS — lightweight canvas rendering | |
| ═══════════════════════════════════════════════ */ | |
| function drawChart(canvasId, data, opts) { | |
| var canvas = document.getElementById(canvasId); | |
| var dpr = window.devicePixelRatio || 1; | |
| var rect = canvas.parentElement.getBoundingClientRect(); | |
| canvas.width = rect.width * dpr; | |
| canvas.height = rect.height * dpr; | |
| canvas.style.width = rect.width + 'px'; | |
| canvas.style.height = rect.height + 'px'; | |
| var ctx = canvas.getContext('2d'); | |
| ctx.scale(dpr, dpr); | |
| var W = rect.width, H = rect.height; | |
| var pad = { top: 28, right: 24, bottom: 40, left: 64 }; | |
| var plotW = W - pad.left - pad.right; | |
| var plotH = H - pad.top - pad.bottom; | |
| var minY = opts.minY !== undefined ? opts.minY : Math.min.apply(null, data); | |
| var maxY = opts.maxY !== undefined ? opts.maxY : Math.max.apply(null, data); | |
| var rangeY = maxY - minY || 1; | |
| function xPos(i) { return pad.left + (i / (data.length - 1)) * plotW; } | |
| function yPos(v) { return pad.top + plotH - ((v - minY) / rangeY) * plotH; } | |
| // Grid lines | |
| ctx.strokeStyle = 'rgba(100,180,255,0.06)'; | |
| ctx.lineWidth = 1; | |
| var gridCount = 5; | |
| for (var g = 0; g <= gridCount; g++) { | |
| var gy = pad.top + (g / gridCount) * plotH; | |
| ctx.beginPath(); ctx.moveTo(pad.left, gy); ctx.lineTo(W - pad.right, gy); ctx.stroke(); | |
| var label = (maxY - (g / gridCount) * rangeY).toFixed(0); | |
| ctx.fillStyle = 'rgba(216,224,236,0.25)'; | |
| ctx.font = '10px IBM Plex Mono'; | |
| ctx.textAlign = 'right'; | |
| ctx.fillText(label, pad.left - 12, gy + 4); | |
| } | |
| // X axis labels | |
| var stepsArr = opts.stepsArray || TRAIN_DATA.steps; | |
| var xInterval = data.length > 30 ? 20 : data.length > 10 ? 5 : 2; | |
| ctx.fillStyle = 'rgba(216,224,236,0.25)'; | |
| ctx.textAlign = 'center'; | |
| for (var x = 0; x < data.length; x += xInterval) { | |
| ctx.fillText(stepsArr[x], xPos(x), H - 8); | |
| } | |
| ctx.fillText(stepsArr[data.length - 1], xPos(data.length - 1), H - 8); | |
| // Zero line for reward chart | |
| if (opts.zeroLine) { | |
| var zy = yPos(0); | |
| if (zy >= pad.top && zy <= pad.top + plotH) { | |
| ctx.strokeStyle = 'rgba(255,255,255,0.12)'; | |
| ctx.setLineDash([4, 4]); | |
| ctx.beginPath(); ctx.moveTo(pad.left, zy); ctx.lineTo(W - pad.right, zy); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| } | |
| } | |
| // Reference lines (baselines) — drawn before data so they appear underneath | |
| if (opts.refLines) { | |
| opts.refLines.forEach(function(ref) { | |
| var ry = yPos(ref.value); | |
| if (ry >= pad.top - 5 && ry <= pad.top + plotH + 5) { | |
| ctx.strokeStyle = ref.color || 'rgba(255,255,255,0.2)'; | |
| ctx.lineWidth = 1.5; | |
| ctx.setLineDash(ref.dash || [6, 4]); | |
| ctx.beginPath(); ctx.moveTo(pad.left, ry); ctx.lineTo(W - pad.right, ry); ctx.stroke(); | |
| ctx.setLineDash([]); | |
| // Draw label with opaque background so it's never covered | |
| var labelY = ref.labelBelow ? ry + 24 : ry - 16; | |
| ctx.font = '600 9px IBM Plex Mono'; | |
| var textWidth = ctx.measureText(ref.label).width; | |
| // Background pill | |
| ctx.fillStyle = 'rgba(6, 10, 17, 0.85)'; | |
| ctx.beginPath(); | |
| ctx.roundRect(W - pad.right - textWidth - 16, labelY - 9, textWidth + 12, 14, 3); | |
| ctx.fill(); | |
| // Label text | |
| ctx.fillStyle = ref.color || 'rgba(255,255,255,0.3)'; | |
| ctx.textAlign = 'right'; | |
| ctx.fillText(ref.label, W - pad.right - 8, labelY); | |
| } | |
| }); | |
| } | |
| // Regions | |
| if (opts.regions) { | |
| opts.regions.forEach(function(r) { | |
| var x1 = xPos(r.from); | |
| var x2 = xPos(r.to); | |
| ctx.fillStyle = r.color; | |
| ctx.fillRect(x1, pad.top, x2 - x1, plotH); | |
| ctx.fillStyle = r.labelColor || 'rgba(216,224,236,0.35)'; | |
| ctx.font = '600 8px IBM Plex Mono'; | |
| ctx.textAlign = 'center'; | |
| ctx.fillText(r.label, (x1 + x2) / 2, pad.top - 6); | |
| }); | |
| } | |
| // Line gradient | |
| var grad = ctx.createLinearGradient(pad.left, 0, W - pad.right, 0); | |
| if (opts.gradientStops) { | |
| opts.gradientStops.forEach(function(s) { grad.addColorStop(s[0], s[1]); }); | |
| } else { | |
| grad.addColorStop(0, opts.color || '#00c4ff'); | |
| grad.addColorStop(1, opts.color || '#00c4ff'); | |
| } | |
| // Area fill | |
| ctx.beginPath(); | |
| ctx.moveTo(xPos(0), yPos(data[0])); | |
| for (var i = 1; i < data.length; i++) ctx.lineTo(xPos(i), yPos(data[i])); | |
| ctx.lineTo(xPos(data.length - 1), yPos(minY)); | |
| ctx.lineTo(xPos(0), yPos(minY)); | |
| ctx.closePath(); | |
| var areaGrad = ctx.createLinearGradient(0, pad.top, 0, pad.top + plotH); | |
| areaGrad.addColorStop(0, (opts.areaColor || 'rgba(0,196,255,0.12)')); | |
| areaGrad.addColorStop(1, 'rgba(0,196,255,0)'); | |
| ctx.fillStyle = areaGrad; | |
| ctx.fill(); | |
| // Line | |
| ctx.strokeStyle = grad; | |
| ctx.lineWidth = 2; | |
| ctx.lineJoin = 'round'; | |
| ctx.beginPath(); | |
| ctx.moveTo(xPos(0), yPos(data[0])); | |
| for (var j = 1; j < data.length; j++) ctx.lineTo(xPos(j), yPos(data[j])); | |
| ctx.stroke(); | |
| // Dot at end | |
| var lastX = xPos(data.length - 1), lastY = yPos(data[data.length - 1]); | |
| ctx.fillStyle = opts.color || '#00c4ff'; | |
| ctx.beginPath(); ctx.arc(lastX, lastY, 3, 0, Math.PI * 2); ctx.fill(); | |
| } | |
| function renderCharts() { | |
| drawChart('rewardChart', TRAIN_DATA.reward, { | |
| minY: -12, maxY: 30, zeroLine: true, | |
| gradientStops: [[0, '#00e676'], [0.18, '#00e676'], [0.22, '#f0a030'], [0.35, '#ff3d5a'], [1, '#ff3d5a']], | |
| areaColor: 'rgba(0,196,255,0.08)', | |
| regions: [ | |
| { from: 0, to: 19, label: 'BASE MODEL', color: 'rgba(0,230,118,0.04)', labelColor: 'rgba(0,230,118,0.5)' }, | |
| { from: 20, to: 35, label: 'DEGRADATION', color: 'rgba(240,160,48,0.04)', labelColor: 'rgba(240,160,48,0.5)' }, | |
| { from: 36, to: 69, label: 'COLLAPSE', color: 'rgba(255,61,90,0.04)', labelColor: 'rgba(255,61,90,0.5)' }, | |
| { from: 70, to: 99, label: 'CLIPPING', color: 'rgba(255,61,90,0.03)', labelColor: 'rgba(255,61,90,0.4)' }, | |
| ], | |
| refLines: [ | |
| { value: 19.5, label: '9B BASELINE +19.5', color: 'rgba(0,230,118,0.5)', dash: [8, 4] }, | |
| ], | |
| }); | |
| drawChart('lengthChart', TRAIN_DATA.completion_length, { | |
| minY: 0, maxY: 2200, | |
| color: '#f0a030', | |
| areaColor: 'rgba(240,160,48,0.08)', | |
| regions: [ | |
| { from: 0, to: 35, label: 'NORMAL', color: 'rgba(0,230,118,0.03)', labelColor: 'rgba(0,230,118,0.4)' }, | |
| { from: 36, to: 68, label: 'SHORT OUTPUT', color: 'rgba(240,160,48,0.04)', labelColor: 'rgba(240,160,48,0.5)' }, | |
| { from: 69, to: 99, label: 'HIT CAP (2048)', color: 'rgba(255,61,90,0.04)', labelColor: 'rgba(255,61,90,0.5)' }, | |
| ], | |
| }); | |
| drawChart('reward1bChart', TRAIN_DATA_1B.reward, { | |
| minY: -12, maxY: 22, zeroLine: true, | |
| stepsArray: TRAIN_DATA_1B.steps, | |
| color: '#00c4ff', | |
| areaColor: 'rgba(0,196,255,0.1)', | |
| gradientStops: [[0, '#ff3d5a'], [0.4, '#f0a030'], [0.7, '#00c4ff'], [1, '#00c4ff']], | |
| refLines: [ | |
| { value: 19.5, label: '9B BASELINE +19.5', color: 'rgba(0,230,118,0.4)', dash: [8, 4] }, | |
| { value: -4.9, label: '1.5B BASELINE -4.9', color: 'rgba(240,160,48,0.5)', dash: [4, 4], labelBelow: true }, | |
| ], | |
| }); | |
| } | |
| window.addEventListener('resize', renderCharts); | |
| setTimeout(renderCharts, 100); | |
| /* ═══════════════════════════════════════════════ | |
| WAR ROOM ENGINE | |
| ═══════════════════════════════════════════════ */ | |
| var ACTION_ICONS = { inspect: '\u2315', ask_specialist: '\u25C9', apply_fix: '\u26A1', submit: '\u2713', reward_breakdown: '\u2261' }; | |
| var state = { step: 0, reward: 0, fixApplied: false, done: false, startTime: null, timerInterval: null }; | |
| function startTimer() { | |
| state.startTime = Date.now(); | |
| state.timerInterval = setInterval(function() { | |
| var e = Math.floor((Date.now() - state.startTime) / 1000); | |
| document.getElementById('episodeTime').textContent = String(Math.floor(e/60)).padStart(2,'0') + ':' + String(e%60).padStart(2,'0'); | |
| }, 1000); | |
| } | |
| function stopTimer() { if (state.timerInterval) clearInterval(state.timerInterval); } | |
| function updateSteps(step) { | |
| document.querySelectorAll('.step-dot').forEach(function(d, i) { | |
| d.className = 'step-dot'; | |
| if (i < step) d.classList.add('used'); | |
| else if (i === step && !state.done) d.classList.add('current'); | |
| }); | |
| } | |
| function updateReward(delta) { | |
| state.reward += delta; | |
| var el = document.getElementById('rewardValue'); | |
| el.textContent = (state.reward >= 0 ? '+' : '') + state.reward.toFixed(2); | |
| el.className = 'vital-value ' + (state.reward >= 0 ? 'emerald' : 'coral'); | |
| } | |
| function setLayerState(id, cls) { | |
| var el = document.getElementById('layer-' + id); | |
| if (el) el.className = 'arch-layer' + (cls ? ' ' + cls : ''); | |
| } | |
| function clearAllLayers() { document.querySelectorAll('.arch-layer').forEach(function(e) { e.className = 'arch-layer'; }); } | |
| function setStatus(text, cls) { var el = document.getElementById('statusBadge'); el.textContent = text; el.className = 'status-badge' + (cls ? ' ' + cls : ''); } | |
| function addLogEntry(opts) { | |
| var idle = document.getElementById('idlePrompt'); if (idle) idle.remove(); | |
| var body = document.getElementById('logBody'); | |
| var entry = document.createElement('div'); entry.className = 'log-entry'; | |
| var iconCls = opts.type === 'ask_specialist' ? 'specialist' : opts.type; | |
| var typeCls = opts.type === 'inspect' ? 'cyan' : opts.type === 'ask_specialist' ? 'purple' : opts.type === 'apply_fix' ? 'amber' : 'emerald'; | |
| var iconEl = document.createElement('div'); iconEl.className = 'log-entry-icon ' + iconCls; iconEl.textContent = ACTION_ICONS[opts.type] || '\u2022'; | |
| var contentEl = document.createElement('div'); contentEl.className = 'log-entry-content'; | |
| var headerEl = document.createElement('div'); headerEl.className = 'log-entry-header'; | |
| var typeEl = document.createElement('div'); typeEl.className = 'log-entry-type ' + typeCls; typeEl.textContent = opts.label; | |
| var stepEl = document.createElement('div'); stepEl.className = 'log-entry-step'; stepEl.textContent = 'Step ' + state.step + '/6'; | |
| headerEl.appendChild(typeEl); headerEl.appendChild(stepEl); | |
| var textEl = document.createElement('div'); textEl.className = 'log-entry-text'; textEl.textContent = opts.text; | |
| contentEl.appendChild(headerEl); contentEl.appendChild(textEl); | |
| if (opts.reward !== undefined) { | |
| var rEl = document.createElement('div'); rEl.className = 'log-entry-reward ' + (opts.reward >= 0 ? 'positive' : 'negative'); | |
| rEl.textContent = (opts.reward >= 0 ? '+' : '') + opts.reward.toFixed(2); contentEl.appendChild(rEl); | |
| } | |
| entry.appendChild(iconEl); entry.appendChild(contentEl); | |
| body.appendChild(entry); body.scrollTop = body.scrollHeight; | |
| } | |
| function addIncidentCard(sc) { | |
| var idle = document.getElementById('idlePrompt'); if (idle) idle.remove(); | |
| var body = document.getElementById('logBody'); | |
| var card = document.createElement('div'); card.className = 'incident-card'; | |
| var label = document.createElement('div'); label.className = 'incident-label'; label.textContent = 'Incident Ticket'; | |
| var text = document.createElement('div'); text.className = 'incident-text'; text.textContent = sc.incident_ticket; | |
| var meta = document.createElement('div'); meta.className = 'incident-meta'; | |
| [['Hardware', sc.hardware], ['Model', sc.model_name], ['Backend', sc.backend]].forEach(function(f) { | |
| var item = document.createElement('div'); item.className = 'incident-meta-item'; | |
| var ml = document.createElement('div'); ml.className = 'meta-label'; ml.textContent = f[0]; | |
| var mv = document.createElement('div'); mv.className = 'meta-value'; mv.textContent = f[1]; | |
| item.appendChild(ml); item.appendChild(mv); meta.appendChild(item); | |
| }); | |
| card.appendChild(label); card.appendChild(text); card.appendChild(meta); body.appendChild(card); | |
| } | |
| function populateSpecialists(ops) { | |
| var body = document.getElementById('specialistsBody'); body.textContent = ''; | |
| var i = 0; | |
| Object.keys(ops).forEach(function(name) { | |
| var d = ops[name]; | |
| var card = document.createElement('div'); card.className = 'specialist-card'; card.id = 'specialist-' + name; | |
| card.style.animationDelay = (i * 0.1) + 's'; | |
| var top = document.createElement('div'); top.className = 'specialist-top'; | |
| var nameEl = document.createElement('div'); nameEl.className = 'specialist-name'; nameEl.textContent = name; | |
| var barW = document.createElement('div'); barW.className = 'confidence-bar'; | |
| var barF = document.createElement('div'); barF.className = 'confidence-fill'; barF.style.width = (d.confidence * 100) + '%'; | |
| barW.appendChild(barF); top.appendChild(nameEl); top.appendChild(barW); | |
| var opEl = document.createElement('div'); opEl.className = 'specialist-opinion'; opEl.textContent = d.opinion; | |
| var vEl = document.createElement('div'); vEl.className = 'specialist-verdict'; | |
| card.appendChild(top); card.appendChild(opEl); card.appendChild(vEl); body.appendChild(card); i++; | |
| }); | |
| } | |
| function highlightSpecialist(name) { | |
| document.querySelectorAll('.specialist-card').forEach(function(c) { c.classList.remove('highlighted'); }); | |
| var c = document.getElementById('specialist-' + name); if (c) c.classList.add('highlighted'); | |
| } | |
| function markSpecialist(name, correct) { | |
| var c = document.getElementById('specialist-' + name); if (!c) return; | |
| c.classList.remove('highlighted'); c.classList.add(correct ? 'correct' : 'wrong'); | |
| var v = c.querySelector('.specialist-verdict'); if (v) v.textContent = correct ? '\u2713 Helpful' : '\u2014 Not Relevant'; | |
| } | |
| function showDiagnosis(d) { | |
| var overlay = document.getElementById('diagnosisOverlay'); | |
| var result = document.getElementById('diagnosisResult'); result.textContent = ''; | |
| [['Root Cause', d.rootCause, d.rcCorrect, d.correctRc], ['Fix', d.fix, d.fixCorrect, d.correctFix]].forEach(function(r) { | |
| var row = document.createElement('div'); row.className = 'diagnosis-row'; | |
| var lbl = document.createElement('div'); lbl.className = 'diagnosis-row-label'; lbl.textContent = r[0]; | |
| var val = document.createElement('div'); val.className = 'diagnosis-row-value ' + (r[2] ? 'correct' : 'wrong'); | |
| val.textContent = r[1] + (r[2] ? ' \u2713' : ' \u2717 \u2192 ' + r[3]); | |
| row.appendChild(lbl); row.appendChild(val); result.appendChild(row); | |
| }); | |
| // Steps used + time | |
| var stepsRow = document.createElement('div'); stepsRow.className = 'diagnosis-row'; | |
| var stepsLbl = document.createElement('div'); stepsLbl.className = 'diagnosis-row-label'; stepsLbl.textContent = 'Steps Used'; | |
| var stepsVal = document.createElement('div'); stepsVal.className = 'diagnosis-row-value'; stepsVal.textContent = state.step + ' / 6'; | |
| stepsRow.appendChild(stepsLbl); stepsRow.appendChild(stepsVal); result.appendChild(stepsRow); | |
| var timeRow = document.createElement('div'); timeRow.className = 'diagnosis-row'; | |
| var timeLbl = document.createElement('div'); timeLbl.className = 'diagnosis-row-label'; timeLbl.textContent = 'Time'; | |
| var elapsed = state.startTime ? Math.round((Date.now() - state.startTime) / 1000) : 0; | |
| var timeVal = document.createElement('div'); timeVal.className = 'diagnosis-row-value'; timeVal.textContent = elapsed + 's'; | |
| timeRow.appendChild(timeLbl); timeRow.appendChild(timeVal); result.appendChild(timeRow); | |
| var rEl = document.getElementById('diagnosisReward'); | |
| rEl.textContent = (d.totalReward >= 0 ? '+' : '') + d.totalReward.toFixed(2); | |
| rEl.style.color = d.totalReward >= 0 ? 'var(--emerald)' : 'var(--coral)'; | |
| overlay.classList.add('visible'); | |
| } | |
| function resetState() { | |
| state = { step: 0, reward: 0, fixApplied: false, done: false, startTime: null, timerInterval: null }; | |
| stopTimer(); | |
| document.getElementById('logBody').textContent = ''; | |
| var logBody = document.getElementById('logBody'); | |
| var idle = document.createElement('div'); idle.className = 'idle-prompt'; idle.id = 'idlePrompt'; | |
| var txt = document.createElement('div'); txt.className = 'idle-text'; txt.textContent = 'Awaiting incident assignment.\nConnect to a live Stack Doctor environment above.'; | |
| idle.appendChild(txt); logBody.appendChild(idle); | |
| document.getElementById('specialistsBody').textContent = ''; | |
| document.getElementById('rewardValue').textContent = '0.00'; document.getElementById('rewardValue').className = 'vital-value'; | |
| document.getElementById('fixStatus').textContent = 'Not Applied'; document.getElementById('fixStatus').className = 'vital-value'; | |
| document.getElementById('rootCauseValue').textContent = '\u2014'; document.getElementById('rootCauseValue').className = 'vital-value'; | |
| document.getElementById('diagnosisValue').textContent = 'Pending'; document.getElementById('diagnosisValue').className = 'vital-value'; | |
| document.getElementById('scenarioId').textContent = '\u2014'; | |
| document.getElementById('episodeTime').textContent = '00:00'; | |
| document.getElementById('diagnosisOverlay').classList.remove('visible'); | |
| setStatus('Standby', ''); clearAllLayers(); updateSteps(0); | |
| setConnStatus('Disconnected', ''); | |
| } | |
| /* ═══════════════════════════════════════════════ | |
| DEMO — arch_guard_01 | |
| ═══════════════════════════════════════════════ */ | |
| var DEMO = { | |
| id: 'arch_guard_01', root_cause: 'arch_guard', correct_fix: 'relax_arch_check', | |
| incident_ticket: "FlashInfer attention kernel fails to launch on newly provisioned DGX Spark nodes. Error: 'Unsupported GPU architecture sm_121'. Identical model config works on H100 nodes.", | |
| hardware: 'NVIDIA SM121 (DGX Spark)', model_name: 'DeepSeek-V3-671B', backend: 'FlashInfer 0.4', | |
| specialist_opinions: { | |
| runtime: { opinion: "CUDA runtime loaded successfully. No runtime issues detected.", confidence: 0.85 }, | |
| dispatch: { opinion: "Architecture check is blocking kernel dispatch. SM121 is not in the supported set despite being SM90-compatible.", confidence: 0.92 }, | |
| kernel: { opinion: "HMMA m16n8k16 instructions available on SM121. Capability check issue.", confidence: 0.88 }, | |
| loader: { opinion: "Model weights loaded correctly. Weight layout is standard.", confidence: 0.80 }, | |
| }, | |
| inspect_logs: "[FlashInfer] GPU: NVIDIA GH200 (sm_121)\n[FlashInfer] is_supported_arch(121) = False\n[FlashInfer] Architecture check FAILED\n[CUDA] All CUDA operations nominal\n[System] GPU memory: 96GB available", | |
| inspect_config: "gpu_architecture: sm_121\ncuda_version: 13.0\nflashinfer_version: 0.4.1\nsupported_archs: [70, 75, 80, 86, 89, 90]", | |
| followup_dispatch: "The dispatch table maps arch -> kernel. SM121 has no entry. Adding sm_12x family to the arch check should resolve this.", | |
| }; | |
| function sleep(ms) { return new Promise(function(r) { setTimeout(r, ms); }); } | |
| /* ══════════════════════════════════════════════ | |
| LIVE ENVIRONMENT CONNECTION | |
| ══════════════════════════════════════════════ */ | |
| var SERVER = { url: '', ws: null }; | |
| function getServerUrl() { return document.getElementById('serverUrl').value.replace(/\/$/, ''); } | |
| function getWsUrl() { | |
| var base = getServerUrl(); | |
| if (!base) { | |
| // Same-origin: derive WS URL from current page | |
| var proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| return proto + '//' + location.host + '/ws'; | |
| } | |
| return base.replace(/^http/, 'ws') + '/ws'; | |
| } | |
| function setConnStatus(text, cls) { | |
| var el = document.getElementById('connStatus'); | |
| el.textContent = text; el.className = 'conn-status' + (cls ? ' ' + cls : ''); | |
| } | |
| /* WebSocket-based communication — maintains session state across reset/step */ | |
| function wsConnect() { | |
| return new Promise(function(resolve, reject) { | |
| var url = getWsUrl(); | |
| var ws = new WebSocket(url); | |
| ws.onopen = function() { SERVER.ws = ws; resolve(ws); }; | |
| ws.onerror = function(e) { reject(new Error('WebSocket connection failed')); }; | |
| ws.onclose = function() { SERVER.ws = null; }; | |
| }); | |
| } | |
| function wsSend(type, data) { | |
| return new Promise(function(resolve, reject) { | |
| if (!SERVER.ws || SERVER.ws.readyState !== WebSocket.OPEN) { | |
| reject(new Error('WebSocket not connected')); return; | |
| } | |
| SERVER.ws.onmessage = function(evt) { | |
| try { | |
| var msg = JSON.parse(evt.data); | |
| resolve(msg); /* Always resolve — caller handles errors */ | |
| } | |
| catch (e) { reject(new Error('Bad JSON from server')); } | |
| }; | |
| SERVER.ws.send(JSON.stringify({ type: type, data: data || {} })); | |
| }); | |
| } | |
| function wsClose() { | |
| if (SERVER.ws) { SERVER.ws.close(); SERVER.ws = null; } | |
| } | |
| /* Convenience wrappers — returns {observation, reward, done} from data envelope */ | |
| async function serverReset(body) { | |
| if (!SERVER.ws) await wsConnect(); | |
| var msg = await wsSend('reset', body || {}); | |
| if (msg.type === 'error') throw new Error((msg.data && msg.data.message) || 'Reset failed'); | |
| return msg.data; /* {observation, reward, done} */ | |
| } | |
| async function serverStep(actionMessage) { | |
| /* WS step format: {type: "step", data: {message: "..."}} — NOT wrapped in action */ | |
| var msg = await wsSend('step', { message: actionMessage }); | |
| if (msg.type === 'error') { | |
| /* Environment validation error (invalid target, specialist, etc.) — return penalty */ | |
| var errMsg = (msg.data && msg.data.message) || 'Unknown error'; | |
| return { observation: { output: 'Error: ' + errMsg }, reward: -2.0, done: false }; | |
| } | |
| return msg.data; /* {observation, reward, done} */ | |
| } | |
| function disableButtons() { | |
| var btn = document.getElementById('demoBtn'); | |
| btn.disabled = true; btn.textContent = '\u25CF Running...'; | |
| } | |
| function enableButtons() { | |
| var btn = document.getElementById('demoBtn'); | |
| btn.disabled = false; btn.textContent = '\u25B6 Run Comparison (Untrained \u2192 Trained)'; | |
| } | |
| /* Map root_cause to a layer name for the architecture diagram */ | |
| var CAUSE_TO_LAYER = { | |
| arch_guard: 'backend', backend_whitelist: 'backend', backend_selector: 'backend', | |
| runtime_loader: 'runtime', driver_compat: 'driver', | |
| model_config: 'model', weight_layout: 'model', | |
| memory_oom: 'memory', quantization_error: 'kernel', | |
| distributed_comm: 'runtime' | |
| }; | |
| /* Parse the environment observation to extract structured info */ | |
| function parseObs(obs) { | |
| return { | |
| output: obs.output || '', | |
| ticket: obs.incident_ticket || '', | |
| hardware: obs.hardware || '', | |
| model: obs.model_name || '', | |
| backend: obs.backend || '', | |
| log: obs.log_excerpt || '', | |
| snippet: obs.code_snippet || '', | |
| specialists: obs.specialist_opinions || {}, | |
| remaining: obs.steps_remaining, | |
| fixUsed: obs.fix_used, | |
| done: obs.done, | |
| reward: obs.reward || 0, | |
| meta: obs.metadata || {} | |
| }; | |
| } | |
| async function runLive(mode) { | |
| disableButtons(); resetState(); setConnStatus('Connecting...', 'running'); | |
| try { | |
| // Step 0: Reset — get a real scenario via WebSocket (stateful session) | |
| var resetResp = await serverReset({}); | |
| var obs = parseObs(resetResp.observation || resetResp); | |
| setConnStatus('Connected', 'connected'); | |
| startTimer(); | |
| setStatus('Incident Received', 'warning'); | |
| // Extract scenario ID from metadata | |
| var scenarioId = (obs.meta && obs.meta.scenario_id) || 'unknown'; | |
| document.getElementById('scenarioId').textContent = scenarioId; | |
| document.getElementById('detail-model').textContent = obs.model; | |
| document.getElementById('detail-backend').textContent = obs.backend; | |
| document.getElementById('detail-driver').textContent = obs.hardware; | |
| // Populate incident card from real data | |
| addIncidentCard({ | |
| incident_ticket: obs.ticket, | |
| hardware: obs.hardware, model_name: obs.model, backend: obs.backend | |
| }); | |
| populateSpecialists(obs.specialists); | |
| await sleep(1800); | |
| if (mode === 'untrained') { | |
| await runLiveUntrained(obs); | |
| } else { | |
| await runLiveTrained(obs); | |
| } | |
| await sleep(6000); | |
| document.getElementById('diagnosisOverlay').classList.remove('visible'); | |
| } catch (e) { | |
| console.error('Live connection failed:', e); | |
| addLogEntry({ type: 'submit', label: 'CONNECTION ERROR', reward: 0, text: e.message + '\n' + (e.stack || '') }); | |
| wsClose(); | |
| setConnStatus('Offline mode', 'running'); | |
| await runOffline(mode); | |
| await sleep(6000); | |
| document.getElementById('diagnosisOverlay').classList.remove('visible'); | |
| } | |
| wsClose(); | |
| enableButtons(); | |
| } | |
| /* ══════════════════════════════════════════════ | |
| OFFLINE FALLBACK (no server needed) | |
| ══════════════════════════════════════════════ */ | |
| async function runOffline(mode) { | |
| var sc = DEMO; | |
| startTimer(); | |
| setStatus('Incident Received', 'warning'); | |
| document.getElementById('scenarioId').textContent = sc.id; | |
| document.getElementById('detail-model').textContent = sc.model_name; | |
| document.getElementById('detail-backend').textContent = sc.backend; | |
| document.getElementById('detail-driver').textContent = sc.hardware; | |
| addIncidentCard(sc); populateSpecialists(sc.specialist_opinions); | |
| await sleep(1800); | |
| if (mode === 'untrained') { await runOfflineUntrained(sc); } | |
| else { await runOfflineTrained(sc); } | |
| } | |
| async function runOfflineUntrained(sc) { | |
| state.step = 1; updateSteps(1); setStatus('Model Acting', 'warning'); | |
| addLogEntry({ type: 'submit', label: 'Blind Submit (no investigation)', reward: 0, | |
| text: 'Model skips investigation.\nOutput: [{"type":"submit","root_cause":"runtime_loader","fix":"fix_runtime_path","justification":"maybe"}]' }); | |
| await sleep(1800); | |
| document.getElementById('rootCauseValue').textContent = 'runtime_loader'; | |
| document.getElementById('rootCauseValue').className = 'vital-value coral'; | |
| setLayerState('runtime', 'identified'); | |
| var totalReward = -11.5; | |
| addLogEntry({ type: 'reward_breakdown', label: 'Reward Breakdown', reward: totalReward, | |
| text: 'Root cause: runtime_loader \u2717 (expected arch_guard) \u2192 -8.0\nFix: fix_runtime_path \u2717 (expected relax_arch_check) \u2192 -2.0\nNo investigation \u2192 -1.0\nJustification too short \u2192 -0.5' }); | |
| updateReward(totalReward); | |
| state.done = true; setStatus('Diagnosis Submitted', 'error'); | |
| document.getElementById('diagnosisValue').textContent = '\u2717 Incorrect'; | |
| document.getElementById('diagnosisValue').className = 'vital-value coral'; | |
| markSpecialist('runtime', false); markSpecialist('dispatch', true); | |
| markSpecialist('kernel', true); markSpecialist('loader', false); | |
| await sleep(1500); stopTimer(); | |
| showDiagnosis({ rcCorrect: false, fixCorrect: false, rootCause: 'runtime_loader', fix: 'fix_runtime_path', | |
| correctRc: 'arch_guard', correctFix: 'relax_arch_check', totalReward: state.reward }); | |
| } | |
| async function runOfflineTrained(sc) { | |
| state.step = 1; updateSteps(1); setStatus('Investigating', 'warning'); | |
| setLayerState('runtime', 'scanning'); | |
| addLogEntry({ type: 'inspect', label: 'Inspect Logs', reward: -0.25, text: sc.inspect_logs }); | |
| updateReward(-0.25); await sleep(2200); | |
| state.step = 2; updateSteps(2); | |
| setLayerState('runtime', ''); setLayerState('backend', 'scanning'); | |
| addLogEntry({ type: 'inspect', label: 'Inspect Config', reward: -0.25, text: sc.inspect_config }); | |
| updateReward(-0.25); await sleep(2000); | |
| state.step = 3; updateSteps(3); | |
| setLayerState('backend', 'identified'); highlightSpecialist('dispatch'); | |
| addLogEntry({ type: 'ask_specialist', label: 'Query: Dispatch', reward: -0.25, text: sc.followup_dispatch }); | |
| updateReward(-0.25); | |
| document.getElementById('rootCauseValue').textContent = 'arch_guard'; | |
| document.getElementById('rootCauseValue').className = 'vital-value amber'; | |
| await sleep(2200); | |
| state.step = 4; updateSteps(4); setStatus('Applying Fix', 'warning'); | |
| state.fixApplied = true; setLayerState('backend', 'resolved'); | |
| addLogEntry({ type: 'apply_fix', label: 'Apply Fix: relax_arch_check', reward: 3.0, text: 'Fix applied successfully. Systems recovering.' }); | |
| updateReward(3.0); | |
| document.getElementById('fixStatus').textContent = '\u2713 Applied'; | |
| document.getElementById('fixStatus').className = 'vital-value emerald'; | |
| ['backend', 'runtime', 'model', 'memory', 'driver'].forEach(function(l, i) { | |
| setTimeout(function() { setLayerState(l, 'resolved'); }, i * 300); | |
| }); await sleep(2000); | |
| state.step = 5; updateSteps(5); state.done = true; | |
| setStatus('Diagnosis Submitted', 'success'); | |
| addLogEntry({ type: 'submit', label: 'Submit Diagnosis', reward: 19.0, | |
| text: 'Root cause: arch_guard \u2713\nFix: relax_arch_check \u2713\nJustification: Logs show sm_121 rejected by arch check. Dispatch confirmed SM121 supports HMMA. Config missing sm_12x in supported_archs.' }); | |
| updateReward(19.0); | |
| document.getElementById('diagnosisValue').textContent = '\u2713 Correct'; | |
| document.getElementById('diagnosisValue').className = 'vital-value emerald'; | |
| markSpecialist('runtime', false); markSpecialist('dispatch', true); | |
| markSpecialist('kernel', true); markSpecialist('loader', false); | |
| await sleep(1500); stopTimer(); | |
| showDiagnosis({ rcCorrect: true, fixCorrect: true, rootCause: 'arch_guard', fix: 'relax_arch_check', | |
| correctRc: 'arch_guard', correctFix: 'relax_arch_check', totalReward: state.reward }); | |
| } | |
| /* ── UNTRAINED: blind submit, no investigation ── */ | |
| async function runLiveUntrained(initObs) { | |
| setStatus('Model Acting', 'warning'); | |
| state.step = 1; updateSteps(1); | |
| // Untrained model skips all investigation — just submits a random wrong guess | |
| addLogEntry({ type: 'inspect', label: 'Untrained Model Behavior', reward: 0, | |
| text: 'Model receives incident but skips investigation.\nNo logs read. No config checked. No specialists queried.\nImmediately submits a blind guess...' }); | |
| await sleep(2000); | |
| // Send a deliberately wrong submit to the real environment | |
| state.step = 2; updateSteps(2); | |
| var stepResp = await serverStep('{"type":"submit","root_cause":"runtime_loader","fix":"fix_runtime_path","justification":"idk"}'); | |
| var obs = parseObs(stepResp.observation || stepResp); | |
| var stepReward = stepResp.reward !== undefined ? stepResp.reward : obs.reward; | |
| setConnStatus('Connected', 'connected'); | |
| // Parse the real environment response | |
| var outputText = obs.output; | |
| var rcCorrect = outputText.indexOf('CORRECT') !== -1 && outputText.indexOf('Root cause') !== -1 | |
| && outputText.split('Root cause')[1].split('\n')[0].indexOf('CORRECT') !== -1; | |
| var fixCorrect = outputText.indexOf('CORRECT') !== -1 && outputText.indexOf('Fix:') !== -1 | |
| && outputText.split('Fix:')[1].split('\n')[0].indexOf('CORRECT') !== -1; | |
| // Extract actual correct answers from output | |
| var correctRc = ''; var correctFix = ''; | |
| var rcMatch = outputText.match(/WRONG \(was: (\w+)\)/); | |
| if (rcMatch) correctRc = rcMatch[1]; | |
| var fixMatch = outputText.match(/Fix:.*WRONG \(was: (\w+)\)/); | |
| if (fixMatch) correctFix = fixMatch[1]; | |
| document.getElementById('rootCauseValue').textContent = 'runtime_loader'; | |
| document.getElementById('rootCauseValue').className = 'vital-value coral'; | |
| addLogEntry({ type: 'submit', label: 'Blind Submit', reward: stepReward, | |
| text: outputText }); | |
| updateReward(stepReward); | |
| state.done = true; | |
| setStatus('Diagnosis Submitted', obs.reward >= 0 ? 'success' : 'error'); | |
| document.getElementById('diagnosisValue').textContent = rcCorrect ? '\u2713 Correct' : '\u2717 Incorrect'; | |
| document.getElementById('diagnosisValue').className = 'vital-value ' + (rcCorrect ? 'emerald' : 'coral'); | |
| await sleep(1500); stopTimer(); | |
| showDiagnosis({ | |
| rcCorrect: rcCorrect, fixCorrect: fixCorrect, | |
| rootCause: 'runtime_loader', fix: 'fix_runtime_path', | |
| correctRc: correctRc || 'unknown', correctFix: correctFix || 'unknown', | |
| totalReward: state.reward | |
| }); | |
| } | |
| /* ── TRAINED: investigate, then diagnose ── */ | |
| async function runLiveTrained(initObs) { | |
| setStatus('Investigating', 'warning'); | |
| // Step 1: Inspect logs | |
| state.step = 1; updateSteps(1); | |
| setLayerState('runtime', 'scanning'); | |
| var step1 = await serverStep('{"type":"inspect","target":"logs"}'); | |
| var obs1 = parseObs(step1.observation || step1); | |
| var rew1 = step1.reward !== undefined ? step1.reward : obs1.reward; | |
| addLogEntry({ type: 'inspect', label: 'Inspect Logs', reward: rew1, text: obs1.output }); | |
| updateReward(rew1); | |
| await sleep(2200); | |
| // Step 2: Inspect config | |
| state.step = 2; updateSteps(2); | |
| setLayerState('runtime', ''); setLayerState('backend', 'scanning'); | |
| var step2 = await serverStep('{"type":"inspect","target":"config"}'); | |
| var obs2 = parseObs(step2.observation || step2); | |
| var rew2 = step2.reward !== undefined ? step2.reward : obs2.reward; | |
| addLogEntry({ type: 'inspect', label: 'Inspect Config', reward: rew2, text: obs2.output }); | |
| updateReward(rew2); | |
| await sleep(2000); | |
| // Step 3: Query a specialist — pick dispatch as a reasonable investigation choice | |
| state.step = 3; updateSteps(3); | |
| setLayerState('backend', 'identified'); | |
| highlightSpecialist('dispatch'); | |
| var step3 = await serverStep('{"type":"ask_specialist","specialist":"dispatch"}'); | |
| var obs3 = parseObs(step3.observation || step3); | |
| var rew3 = step3.reward !== undefined ? step3.reward : obs3.reward; | |
| addLogEntry({ type: 'ask_specialist', label: 'Query: Dispatch', reward: rew3, text: obs3.output }); | |
| updateReward(rew3); | |
| await sleep(2200); | |
| // Step 4: Smart submit — analyze the logs/config to guess the right answer | |
| // For the demo, we use the scenario hints from the environment output to make an informed guess | |
| // A real trained model would parse the observations and infer the root cause | |
| state.step = 4; updateSteps(4); setStatus('Diagnosing', 'warning'); | |
| // Attempt to extract the root cause from clues in the observations | |
| var allText = obs1.output + ' ' + obs2.output + ' ' + obs3.output; | |
| var guessRc = inferRootCause(allText); | |
| var guessFix = RC_TO_FIX[guessRc] || 'switch_backend'; | |
| var justification = 'Logs and config analysis indicates ' + guessRc + '. Dispatch specialist confirmed. Applying ' + guessFix + '.'; | |
| document.getElementById('rootCauseValue').textContent = guessRc; | |
| document.getElementById('rootCauseValue').className = 'vital-value amber'; | |
| // Apply fix first | |
| var step4 = await serverStep(JSON.stringify({ type: 'apply_fix', fix: guessFix })); | |
| var obs4 = parseObs(step4.observation || step4); | |
| var rew4 = step4.reward !== undefined ? step4.reward : obs4.reward; | |
| var fixWorked = rew4 > 0; | |
| addLogEntry({ type: 'apply_fix', label: 'Apply Fix: ' + guessFix, reward: rew4, text: obs4.output }); | |
| updateReward(rew4); | |
| document.getElementById('fixStatus').textContent = fixWorked ? '\u2713 Applied' : '\u2717 Failed'; | |
| document.getElementById('fixStatus').className = 'vital-value ' + (fixWorked ? 'emerald' : 'coral'); | |
| if (fixWorked) { | |
| var layers = ['backend', 'runtime', 'model', 'memory', 'driver']; | |
| layers.forEach(function(l, i) { setTimeout(function() { setLayerState(l, 'resolved'); }, i * 300); }); | |
| } | |
| await sleep(2000); | |
| // Step 5: Submit diagnosis | |
| var isDone4 = step4.done !== undefined ? step4.done : obs4.done; | |
| if (!isDone4) { | |
| state.step = 5; updateSteps(5); | |
| var step5 = await serverStep(JSON.stringify({ type: 'submit', root_cause: guessRc, fix: guessFix, justification: justification })); | |
| var obs5 = parseObs(step5.observation || step5); | |
| var rew5 = step5.reward !== undefined ? step5.reward : obs5.reward; | |
| addLogEntry({ type: 'submit', label: 'Submit Diagnosis', reward: rew5, text: obs5.output }); | |
| updateReward(rew5); | |
| var outputText = obs5.output; | |
| var rcCorrect = outputText.indexOf('Root cause') !== -1 && outputText.split('Root cause')[1].split('\n')[0].indexOf('CORRECT') !== -1; | |
| var fixCorrect2 = outputText.indexOf('Fix:') !== -1 && outputText.split('Fix:')[1].split('\n')[0].indexOf('CORRECT') !== -1; | |
| state.done = true; | |
| setStatus('Diagnosis Submitted', rcCorrect ? 'success' : 'error'); | |
| document.getElementById('diagnosisValue').textContent = rcCorrect ? '\u2713 Correct' : '\u2717 Incorrect'; | |
| document.getElementById('diagnosisValue').className = 'vital-value ' + (rcCorrect ? 'emerald' : 'coral'); | |
| var correctRc = guessRc; var correctFix = guessFix; | |
| var rcWrong = outputText.match(/WRONG \(was: (\w+)\)/); | |
| if (rcWrong) correctRc = rcWrong[1]; | |
| var fixWrong = outputText.match(/Fix:.*WRONG \(was: (\w+)\)/); | |
| if (fixWrong) correctFix = fixWrong[1]; | |
| await sleep(1500); stopTimer(); | |
| showDiagnosis({ | |
| rcCorrect: rcCorrect && !rcWrong, fixCorrect: fixCorrect2 && !fixWrong, | |
| rootCause: guessRc, fix: guessFix, | |
| correctRc: correctRc, correctFix: correctFix, | |
| totalReward: state.reward | |
| }); | |
| } else { | |
| state.done = true; stopTimer(); | |
| } | |
| } | |
| /* ══════════════════════════════════════════════ | |
| LIVE MODEL INFERENCE — real Qwen 1.5B via MLX | |
| ══════════════════════════════════════════════ */ | |
| /* Inference URL: same origin on HF Spaces, localhost:8001 locally */ | |
| var INFERENCE_URL = (location.hostname === 'localhost' || location.hostname === '127.0.0.1') ? 'http://localhost:8001' : ''; | |
| function extractActionsJS(text) { | |
| text = text.replace(/<think>[\s\S]*?<\/think>/g, '').trim(); | |
| var start = text.indexOf('['), end = text.lastIndexOf(']'); | |
| if (start !== -1 && end > start) { | |
| try { var a = JSON.parse(text.slice(start, end+1)); if (Array.isArray(a)) return a.filter(function(x){return typeof x==='object';}); } catch(e) {} | |
| } | |
| try { var a = JSON.parse(text); if (Array.isArray(a)) return a.filter(function(x){return typeof x==='object';}); if (typeof a === 'object') return [a]; } catch(e) {} | |
| return null; | |
| } | |
| var UNTRAINED_SYSTEM = 'You are Stack Doctor, an expert AI agent that diagnoses inference-stack incidents.\nYou receive an incident ticket with hardware/model/backend context, log excerpts, and specialist opinions.\nSome specialists may be wrong. Output a JSON array of actions:\n {"type":"inspect","target":"logs|config|snippet|metrics"}\n {"type":"ask_specialist","specialist":"runtime|dispatch|kernel|loader"}\n {"type":"apply_fix","fix":"<fix_name>"}\n {"type":"submit","root_cause":"<cause>","fix":"<fix>","justification":"<why>"}'; | |
| var TRAINED_SYSTEM = 'You are Stack Doctor, an expert AI agent that diagnoses inference-stack incidents.\nYou are methodical: first inspect logs and config, then query specialists to cross-verify (some lie), then apply a fix and submit.\n\nAvailable actions (output as a JSON array):\n {"type":"inspect","target":"logs"} or "config" or "snippet" or "metrics"\n {"type":"ask_specialist","specialist":"runtime"} or "dispatch" or "kernel" or "loader"\n {"type":"apply_fix","fix":"<name>"} — available fixes: add_whitelist_entry, fix_comm_config, fix_quantization, fix_runtime_path, fix_weight_mapping, relax_arch_check, switch_backend, tune_memory_config, update_driver_config, update_model_config\n {"type":"submit","root_cause":"<cause>","fix":"<fix>","justification":"<detailed reasoning>"}\n\nAvailable root causes: arch_guard, backend_selector, backend_whitelist, distributed_comm, driver_compat, memory_oom, model_config, quantization_error, runtime_loader, weight_layout\n\nIMPORTANT: Pick ONE target per inspect, ONE specialist per query. Investigate before submitting. Give a detailed justification.\n\nExample output:\n[{"type":"inspect","target":"logs"},{"type":"inspect","target":"config"},{"type":"ask_specialist","specialist":"kernel"},{"type":"apply_fix","fix":"relax_arch_check"},{"type":"submit","root_cause":"arch_guard","fix":"relax_arch_check","justification":"Logs show architecture check failure for SM90 on the backend. Config confirms the guard is enabled. Kernel specialist confirmed this is not a kernel issue. Relaxing the arch check resolves the incompatibility."}]'; | |
| async function runComparison() { | |
| disableButtons(); | |
| // 1. Pick a random scenario ID to pin both runs to the same problem | |
| var scenarioIds = ['arch_guard_01','arch_guard_02','backend_whitelist_01','backend_whitelist_02','runtime_loader_01','runtime_loader_02','backend_selector_01']; | |
| var scenarioId = scenarioIds[Math.floor(Math.random() * scenarioIds.length)]; | |
| // 2. Run untrained | |
| await runLiveModel('untrained', scenarioId); | |
| // 3. Pause between runs | |
| await sleep(3000); | |
| addLogEntry({ type: 'inspect', label: '--- Now running TRAINED model on same scenario ---', reward: 0, text: 'Same incident: ' + scenarioId }); | |
| await sleep(2000); | |
| resetState(); | |
| // 4. Run trained on same scenario | |
| await runLiveModel('trained', scenarioId); | |
| enableButtons(); | |
| } | |
| async function runLiveModel(mode, scenarioId) { | |
| resetState(); setConnStatus('Connecting...', 'running'); | |
| var isTrained = mode === 'trained'; | |
| var ws = null; | |
| try { | |
| // 1. Raw WebSocket connect (derive WS URL from server URL or same-origin) | |
| var base = getServerUrl(); | |
| var wsUrl; | |
| if (!base) { | |
| var proto = location.protocol === 'https:' ? 'wss:' : 'ws:'; | |
| wsUrl = proto + '//' + location.host + '/ws'; | |
| } else { | |
| wsUrl = base.replace(/^http/, 'ws') + '/ws'; | |
| } | |
| ws = new WebSocket(wsUrl); | |
| await new Promise(function(res, rej) { | |
| ws.onopen = res; | |
| ws.onerror = function() { rej(new Error('WebSocket connect failed')); }; | |
| setTimeout(function() { rej(new Error('WebSocket timeout')); }, 5000); | |
| }); | |
| function wsSendRaw(type, data) { | |
| return new Promise(function(res, rej) { | |
| ws.onmessage = function(e) { res(JSON.parse(e.data)); }; | |
| ws.send(JSON.stringify({type: type, data: data || {}})); | |
| setTimeout(function() { rej(new Error('WS ' + type + ' timeout')); }, 15000); | |
| }); | |
| } | |
| // 2. Reset with pinned scenario ID (same scenario for both runs) | |
| var resetData = scenarioId ? { scenario_id: scenarioId } : {}; | |
| var resetMsg = await wsSendRaw('reset', resetData); | |
| if (resetMsg.type === 'error') throw new Error('Reset: ' + JSON.stringify(resetMsg.data)); | |
| var obs = resetMsg.data.observation; | |
| setConnStatus('Connected', 'connected'); | |
| startTimer(); | |
| setStatus('Incident Received', 'warning'); | |
| document.getElementById('scenarioId').textContent = 'live'; | |
| document.getElementById('detail-model').textContent = obs.model_name || ''; | |
| document.getElementById('detail-backend').textContent = obs.backend || ''; | |
| document.getElementById('detail-driver').textContent = obs.hardware || ''; | |
| addIncidentCard({ incident_ticket: obs.incident_ticket, hardware: obs.hardware, model_name: obs.model_name, backend: obs.backend }); | |
| populateSpecialists(obs.specialist_opinions || {}); | |
| await sleep(1000); | |
| // 3. Build prompt from raw observation | |
| var opsStr = ''; | |
| var specs = obs.specialist_opinions || {}; | |
| for (var name in specs) { | |
| var o = specs[name]; | |
| opsStr += ' ' + name + ': ' + o.opinion + ' (confidence: ' + o.confidence + ')\n'; | |
| } | |
| var userPrompt = 'INCIDENT: ' + obs.incident_ticket + '\nHardware: ' + obs.hardware + ' | Model: ' + obs.model_name + ' | Backend: ' + obs.backend + '\nLOG:\n' + obs.log_excerpt + '\nSPECIALISTS:\n' + opsStr + '\nInvestigate and submit your diagnosis as a JSON action array.'; | |
| // 4. Call inference server | |
| setStatus(isTrained ? 'Trained Model Thinking...' : 'Untrained Model Thinking...', 'warning'); | |
| addLogEntry({ type: 'inspect', label: 'Qwen 1.5B ' + (isTrained ? '(GRPO)' : '(Base)') + ' Generating...', reward: 0, text: 'Sending scenario to local MLX model for inference...' }); | |
| var genResp = await fetch(INFERENCE_URL + '/generate', { | |
| method: 'POST', headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ prompt: userPrompt, max_tokens: 512, mode: isTrained ? 'trained' : 'untrained', system: isTrained ? TRAINED_SYSTEM : UNTRAINED_SYSTEM }) | |
| }).then(function(r) { return r.json(); }); | |
| addLogEntry({ type: 'inspect', label: 'Model Output (' + genResp.gen_time.toFixed(1) + 's)', reward: 0, text: genResp.text.slice(0, 500) }); | |
| await sleep(500); | |
| // 5. Parse actions | |
| var actions = extractActionsJS(genResp.text); | |
| if (!actions || actions.length === 0) { | |
| addLogEntry({ type: 'submit', label: 'Parse Failed', reward: -5, text: 'Could not parse model output as JSON actions.\nRaw: ' + genResp.text.slice(0, 200) }); | |
| updateReward(-5); | |
| state.done = true; setStatus('Parse Error', 'error'); stopTimer(); | |
| ws.close(); enableButtons(); return; | |
| } | |
| setStatus('Executing Actions', 'warning'); | |
| // 6. Execute each action via raw WebSocket | |
| var done = false; | |
| var lastOutput = ''; | |
| var totalReward = 0; | |
| for (var i = 0; i < actions.length && !done; i++) { | |
| var action = actions[i]; | |
| var aType = action.type || '?'; | |
| state.step = i + 1; updateSteps(i + 1); | |
| // Visual feedback on architecture diagram | |
| if (aType === 'inspect') setLayerState('runtime', 'scanning'); | |
| if (aType === 'ask_specialist') highlightSpecialist(action.specialist || 'dispatch'); | |
| if (aType === 'apply_fix') setLayerState('backend', 'identified'); | |
| if (aType === 'submit') { setStatus('Diagnosing', 'warning'); document.getElementById('rootCauseValue').textContent = action.root_cause || '?'; } | |
| var stepMsg = await wsSendRaw('step', { message: JSON.stringify(action) }); | |
| var rew = 0; | |
| var stepOutput = ''; | |
| if (stepMsg.type === 'error') { | |
| rew = -2; | |
| stepOutput = 'Error: ' + ((stepMsg.data && stepMsg.data.message) || 'unknown'); | |
| } else { | |
| rew = stepMsg.data.reward || 0; | |
| done = stepMsg.data.done || false; | |
| stepOutput = (stepMsg.data.observation && stepMsg.data.observation.output) || ''; | |
| } | |
| totalReward += rew; | |
| var label = aType; | |
| if (aType === 'inspect') label = 'Inspect: ' + (action.target || '?'); | |
| if (aType === 'ask_specialist') label = 'Query: ' + (action.specialist || '?'); | |
| if (aType === 'apply_fix') label = 'Fix: ' + (action.fix || '?'); | |
| if (aType === 'submit') label = 'Submit Diagnosis'; | |
| addLogEntry({ type: aType, label: label, reward: rew, text: stepOutput }); | |
| updateReward(rew); | |
| lastOutput = stepOutput; | |
| // Green up layers on successful fix | |
| if (aType === 'apply_fix' && rew > 0) { | |
| ['backend', 'runtime', 'model', 'memory', 'driver'].forEach(function(l, idx) { | |
| setTimeout(function() { setLayerState(l, 'resolved'); }, idx * 300); | |
| }); | |
| } | |
| await sleep(1200); | |
| } | |
| // 7. Final diagnosis | |
| state.done = true; stopTimer(); | |
| if (lastOutput.indexOf('DIAGNOSIS SUBMITTED') !== -1 || lastOutput.indexOf('Root cause') !== -1 || done) { | |
| var rcCorrect = lastOutput.indexOf('CORRECT') !== -1 && lastOutput.indexOf('Root cause') !== -1 && lastOutput.split('Root cause')[1].split('\n')[0].indexOf('CORRECT') !== -1; | |
| setStatus('Diagnosis Submitted', totalReward > 0 ? 'success' : 'error'); | |
| document.getElementById('diagnosisValue').textContent = totalReward > 0 ? '\u2713 Correct' : '\u2717 Incorrect'; | |
| document.getElementById('diagnosisValue').className = 'vital-value ' + (totalReward > 0 ? 'emerald' : 'coral'); | |
| var submitAction = actions.find(function(a) { return a.type === 'submit'; }) || {}; | |
| var correctRc = submitAction.root_cause || '?'; | |
| var correctFix = submitAction.fix || '?'; | |
| var rcWrong = lastOutput.match(/WRONG \(was: (\w+)\)/); | |
| if (rcWrong) correctRc = rcWrong[1]; | |
| var fixWrong = lastOutput.match(/Fix:.*WRONG \(was: (\w+)\)/); | |
| if (fixWrong) correctFix = fixWrong[1]; | |
| showDiagnosis({ | |
| rcCorrect: rcCorrect && !rcWrong, fixCorrect: !fixWrong, | |
| rootCause: submitAction.root_cause || '?', fix: submitAction.fix || '?', | |
| correctRc: correctRc, correctFix: correctFix, | |
| totalReward: state.reward | |
| }); | |
| await sleep(8000); | |
| document.getElementById('diagnosisOverlay').classList.remove('visible'); | |
| } else { | |
| setStatus('Episode Ended', totalReward > 0 ? 'success' : 'error'); | |
| } | |
| } catch (e) { | |
| console.error('Live model error:', e); | |
| setConnStatus('Error: ' + e.message, 'error'); | |
| setStatus('Error', 'error'); | |
| alert('ERROR: ' + e.message); | |
| addLogEntry({ type: 'submit', label: 'ERROR', reward: 0, text: e.message + '\n' + (e.stack || '') }); | |
| } | |
| if (ws) ws.close(); | |
| enableButtons(); | |
| } | |
| /* Root cause inference from observation text — pattern matching on known signatures */ | |
| var RC_TO_FIX = { | |
| arch_guard: 'relax_arch_check', backend_whitelist: 'add_whitelist_entry', | |
| runtime_loader: 'fix_runtime_path', backend_selector: 'switch_backend', | |
| model_config: 'update_model_config', weight_layout: 'fix_weight_mapping', | |
| memory_oom: 'tune_memory_config', quantization_error: 'fix_quantization', | |
| distributed_comm: 'fix_comm_config', driver_compat: 'update_driver_config' | |
| }; | |
| function inferRootCause(text) { | |
| var t = text.toLowerCase(); | |
| if (t.indexOf('arch') !== -1 && (t.indexOf('guard') !== -1 || t.indexOf('unsupported') !== -1 || t.indexOf('architecture check') !== -1)) return 'arch_guard'; | |
| if (t.indexOf('whitelist') !== -1 || t.indexOf('not in supported') !== -1) return 'backend_whitelist'; | |
| if (t.indexOf('runtime') !== -1 && (t.indexOf('loader') !== -1 || t.indexOf('path') !== -1 || t.indexOf('dlopen') !== -1)) return 'runtime_loader'; | |
| if (t.indexOf('backend') !== -1 && (t.indexOf('selector') !== -1 || t.indexOf('fallback') !== -1)) return 'backend_selector'; | |
| if (t.indexOf('model') !== -1 && t.indexOf('config') !== -1 && (t.indexOf('mismatch') !== -1 || t.indexOf('invalid') !== -1)) return 'model_config'; | |
| if (t.indexOf('weight') !== -1 && (t.indexOf('layout') !== -1 || t.indexOf('shape') !== -1)) return 'weight_layout'; | |
| if (t.indexOf('oom') !== -1 || t.indexOf('out of memory') !== -1 || t.indexOf('memory') !== -1 && t.indexOf('exceed') !== -1) return 'memory_oom'; | |
| if (t.indexOf('quantiz') !== -1 || t.indexOf('quant') !== -1 && t.indexOf('error') !== -1) return 'quantization_error'; | |
| if (t.indexOf('nccl') !== -1 || t.indexOf('distributed') !== -1 || t.indexOf('comm') !== -1) return 'distributed_comm'; | |
| if (t.indexOf('driver') !== -1 && (t.indexOf('compat') !== -1 || t.indexOf('version') !== -1)) return 'driver_compat'; | |
| return 'arch_guard'; // fallback | |
| } | |
| /* Poll model status on page load */ | |
| (function checkModelStatus() { | |
| var statusUrl = INFERENCE_URL || ''; | |
| var el = document.getElementById('modelStatus'); | |
| fetch(statusUrl + '/model_status').then(function(r) { return r.json(); }).then(function(d) { | |
| if (d.ready) { | |
| el.textContent = 'Model: ready'; | |
| el.className = 'conn-status connected'; | |
| } else if (d.error) { | |
| el.textContent = 'Model: error'; | |
| el.className = 'conn-status error'; | |
| } else { | |
| el.textContent = 'Model: loading...'; | |
| el.className = 'conn-status running'; | |
| setTimeout(checkModelStatus, 3000); | |
| } | |
| }).catch(function() { | |
| /* Local dev without /model_status endpoint — assume ready */ | |
| el.textContent = 'Model: ready'; | |
| el.className = 'conn-status connected'; | |
| }); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |