Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>DataMedic - AI Data Cleaning Monitor</title> | |
| <style> | |
| :root { | |
| --bg: #050d1a; | |
| --bg2: #0a1628; | |
| --bg3: #0f1f38; | |
| --border: #1a3050; | |
| --green: #00e5a0; | |
| --green-dim: #00704e; | |
| --amber: #f5a623; | |
| --red: #ff4d6d; | |
| --blue: #4db8ff; | |
| --text: #c8dff5; | |
| --text-dim: #4a6a8a; | |
| --mono: 'Courier New', Courier, monospace; | |
| --sans: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| } | |
| * { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| body { | |
| background: var(--bg); | |
| color: var(--text); | |
| font-family: var(--sans); | |
| min-height: 100vh; | |
| overflow-x: hidden; | |
| } | |
| body::before { | |
| content: ''; | |
| position: fixed; | |
| inset: 0; | |
| background: repeating-linear-gradient(0deg, transparent, transparent 2px, | |
| rgba(0, 0, 0, 0.06) 2px, rgba(0, 0, 0, 0.06) 4px); | |
| pointer-events: none; | |
| z-index: 999; | |
| } | |
| /* ββ Header ββ */ | |
| header { | |
| display: flex; | |
| align-items: center; | |
| justify-content: space-between; | |
| padding: 14px 28px; | |
| border-bottom: 1px solid var(--border); | |
| background: var(--bg2); | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| } | |
| .logo { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| .logo-pulse { | |
| width: 10px; | |
| height: 10px; | |
| background: var(--green); | |
| border-radius: 50%; | |
| box-shadow: 0 0 10px var(--green); | |
| animation: pulse 2s infinite; | |
| flex-shrink: 0; | |
| } | |
| @keyframes pulse { | |
| 0%, | |
| 100% { | |
| opacity: 1; | |
| transform: scale(1); | |
| } | |
| 50% { | |
| opacity: 0.3; | |
| transform: scale(0.7); | |
| } | |
| } | |
| .logo-text { | |
| font-family: var(--mono); | |
| font-size: 17px; | |
| font-weight: 700; | |
| letter-spacing: 3px; | |
| color: var(--green); | |
| } | |
| .logo-sub { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| margin-top: 2px; | |
| } | |
| .status-pill { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 4px 14px; | |
| border-radius: 20px; | |
| border: 1px solid; | |
| letter-spacing: 1px; | |
| text-transform: uppercase; | |
| } | |
| .status-pill.idle { | |
| color: var(--text-dim); | |
| border-color: var(--text-dim); | |
| } | |
| .status-pill.running { | |
| color: var(--green); | |
| border-color: var(--green); | |
| box-shadow: 0 0 8px rgba(0, 229, 160, 0.3); | |
| animation: pulse 1s infinite; | |
| } | |
| .status-pill.done { | |
| color: var(--blue); | |
| border-color: var(--blue); | |
| } | |
| /* ββ Controls ββ */ | |
| .controls { | |
| padding: 16px 28px; | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| border-bottom: 1px solid var(--border); | |
| flex-wrap: wrap; | |
| background: var(--bg2); | |
| } | |
| .ctrl-label { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| white-space: nowrap; | |
| } | |
| .task-btn { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 7px 16px; | |
| border-radius: 4px; | |
| border: 1px solid var(--border); | |
| background: var(--bg3); | |
| color: var(--text-dim); | |
| cursor: pointer; | |
| transition: all 0.2s; | |
| letter-spacing: 1px; | |
| } | |
| .task-btn:hover { | |
| border-color: var(--green); | |
| color: var(--green); | |
| } | |
| .task-btn.active { | |
| border-color: var(--green); | |
| color: var(--green); | |
| background: rgba(0, 229, 160, 0.08); | |
| } | |
| .sep { | |
| width: 1px; | |
| height: 24px; | |
| background: var(--border); | |
| margin: 0 4px; | |
| } | |
| .reset-btn { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 7px 16px; | |
| border-radius: 4px; | |
| border: 1px solid var(--amber); | |
| background: transparent; | |
| color: var(--amber); | |
| cursor: pointer; | |
| letter-spacing: 1px; | |
| transition: all 0.2s; | |
| } | |
| .reset-btn:hover { | |
| background: rgba(245, 166, 35, 0.1); | |
| } | |
| .reset-btn:disabled { | |
| opacity: 0.4; | |
| cursor: not-allowed; | |
| } | |
| .run-btn { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| padding: 7px 20px; | |
| border-radius: 4px; | |
| border: none; | |
| background: var(--green); | |
| color: #050d1a; | |
| cursor: pointer; | |
| font-weight: 700; | |
| letter-spacing: 1px; | |
| transition: all 0.2s; | |
| margin-left: auto; | |
| } | |
| .run-btn:hover { | |
| background: #00ffb3; | |
| box-shadow: 0 0 16px rgba(0, 229, 160, 0.4); | |
| } | |
| .run-btn:disabled { | |
| background: var(--green-dim); | |
| cursor: not-allowed; | |
| opacity: 0.5; | |
| } | |
| .run-hint { | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| font-family: var(--mono); | |
| white-space: nowrap; | |
| } | |
| /* ββ Main grid ββ */ | |
| .main { | |
| display: grid; | |
| grid-template-columns: 320px 1fr; | |
| min-height: calc(100vh - 118px); | |
| } | |
| /* ββ Vitals panel ββ */ | |
| .vitals-panel { | |
| border-right: 1px solid var(--border); | |
| padding: 20px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 18px; | |
| overflow-y: auto; | |
| } | |
| .panel-title { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 2px; | |
| padding-bottom: 10px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| /* Score ring */ | |
| .score-ring-wrap { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| gap: 6px; | |
| padding: 8px 0; | |
| } | |
| .ring-container { | |
| position: relative; | |
| width: 130px; | |
| height: 130px; | |
| } | |
| .ring-container svg { | |
| transform: rotate(-90deg); | |
| width: 130px; | |
| height: 130px; | |
| } | |
| .ring-bg { | |
| fill: none; | |
| stroke: var(--bg3); | |
| stroke-width: 10; | |
| } | |
| .ring-fill { | |
| fill: none; | |
| stroke: var(--green); | |
| stroke-width: 10; | |
| stroke-linecap: round; | |
| stroke-dasharray: 326.73; | |
| stroke-dashoffset: 326.73; | |
| transition: stroke-dashoffset 0.7s cubic-bezier(0.4, 0, 0.2, 1), stroke 0.4s; | |
| filter: drop-shadow(0 0 5px var(--green)); | |
| } | |
| .ring-text { | |
| position: absolute; | |
| inset: 0; | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| font-family: var(--mono); | |
| } | |
| .ring-score { | |
| font-size: 28px; | |
| font-weight: 700; | |
| color: var(--green); | |
| line-height: 1; | |
| } | |
| .ring-label { | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| margin-top: 4px; | |
| } | |
| /* Vital grid */ | |
| .vital-grid { | |
| display: grid; | |
| grid-template-columns: 1fr 1fr; | |
| gap: 8px; | |
| } | |
| .vital-card { | |
| background: var(--bg2); | |
| border: 1px solid var(--border); | |
| border-radius: 5px; | |
| padding: 10px; | |
| } | |
| .vital-name { | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| font-family: var(--mono); | |
| margin-bottom: 5px; | |
| } | |
| .vital-value { | |
| font-family: var(--mono); | |
| font-size: 20px; | |
| font-weight: 700; | |
| line-height: 1; | |
| } | |
| .vital-value.green { | |
| color: var(--green); | |
| } | |
| .vital-value.amber { | |
| color: var(--amber); | |
| } | |
| .vital-value.red { | |
| color: var(--red); | |
| } | |
| .vital-value.blue { | |
| color: var(--blue); | |
| } | |
| .vital-sub { | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| margin-top: 3px; | |
| font-family: var(--mono); | |
| } | |
| /* DQ bars */ | |
| .dq-bars { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| } | |
| .dq-row { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 4px; | |
| } | |
| .dq-header { | |
| display: flex; | |
| justify-content: space-between; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| } | |
| .dq-name { | |
| color: var(--text-dim); | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| } | |
| .dq-val { | |
| font-weight: 700; | |
| } | |
| .dq-bar-bg { | |
| height: 4px; | |
| background: var(--bg3); | |
| border-radius: 2px; | |
| overflow: hidden; | |
| } | |
| .dq-bar-fill { | |
| height: 100%; | |
| border-radius: 2px; | |
| transition: width 0.5s cubic-bezier(0.4, 0, 0.2, 1); | |
| } | |
| /* ββ Content area ββ */ | |
| .content-area { | |
| display: flex; | |
| flex-direction: column; | |
| overflow: hidden; | |
| } | |
| /* Chart */ | |
| .chart-section { | |
| padding: 20px 28px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .chart-wrap { | |
| margin-top: 14px; | |
| height: 90px; | |
| position: relative; | |
| } | |
| #score-chart { | |
| width: 100%; | |
| height: 100%; | |
| } | |
| /* Plan */ | |
| .plan-section { | |
| padding: 16px 28px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| .plan-items { | |
| margin-top: 10px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .plan-item { | |
| display: flex; | |
| align-items: flex-start; | |
| gap: 10px; | |
| font-size: 12px; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .plan-num { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| width: 18px; | |
| height: 18px; | |
| border: 1px solid var(--amber); | |
| border-radius: 50%; | |
| display: flex; | |
| align-items: center; | |
| justify-content: center; | |
| flex-shrink: 0; | |
| color: var(--amber); | |
| margin-top: 1px; | |
| } | |
| @keyframes fadeIn { | |
| from { | |
| opacity: 0; | |
| transform: translateY(6px); | |
| } | |
| to { | |
| opacity: 1; | |
| transform: translateY(0); | |
| } | |
| } | |
| /* Thought stream */ | |
| .thought-section { | |
| padding: 16px 28px; | |
| border-bottom: 1px solid var(--border); | |
| flex: 1; | |
| } | |
| .thought-stream { | |
| margin-top: 10px; | |
| display: flex; | |
| flex-direction: column; | |
| gap: 7px; | |
| max-height: 200px; | |
| overflow-y: auto; | |
| } | |
| .thought-stream::-webkit-scrollbar { | |
| width: 3px; | |
| } | |
| .thought-stream::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 2px; | |
| } | |
| .thought-item { | |
| display: flex; | |
| gap: 10px; | |
| align-items: flex-start; | |
| animation: fadeIn 0.3s ease; | |
| } | |
| .thought-step { | |
| font-family: var(--mono); | |
| font-size: 9px; | |
| color: var(--text-dim); | |
| padding: 2px 5px; | |
| border: 1px solid var(--border); | |
| border-radius: 3px; | |
| white-space: nowrap; | |
| margin-top: 1px; | |
| flex-shrink: 0; | |
| } | |
| .thought-body { | |
| flex: 1; | |
| min-width: 0; | |
| } | |
| .thought-action { | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| color: var(--blue); | |
| margin-bottom: 2px; | |
| word-break: break-all; | |
| } | |
| .thought-result { | |
| font-size: 11px; | |
| color: var(--text-dim); | |
| } | |
| .thought-reward { | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| padding: 2px 7px; | |
| border-radius: 3px; | |
| margin-top: 2px; | |
| display: inline-block; | |
| } | |
| .reward-pos { | |
| background: rgba(0, 229, 160, 0.12); | |
| color: var(--green); | |
| } | |
| .reward-neg { | |
| background: rgba(255, 77, 109, 0.12); | |
| color: var(--red); | |
| } | |
| /* Data table */ | |
| .preview-section { | |
| padding: 16px 28px 20px; | |
| } | |
| .data-table-wrap { | |
| margin-top: 10px; | |
| overflow-x: auto; | |
| border: 1px solid var(--border); | |
| border-radius: 5px; | |
| max-height: 220px; | |
| overflow-y: auto; | |
| } | |
| .data-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-family: var(--mono); | |
| font-size: 11px; | |
| } | |
| .data-table th { | |
| background: var(--bg3); | |
| color: var(--text-dim); | |
| padding: 7px 10px; | |
| text-align: left; | |
| text-transform: uppercase; | |
| letter-spacing: 1px; | |
| border-bottom: 1px solid var(--border); | |
| white-space: nowrap; | |
| position: sticky; | |
| top: 0; | |
| } | |
| .data-table td { | |
| padding: 5px 10px; | |
| border-bottom: 1px solid rgba(26, 48, 80, 0.4); | |
| color: var(--text); | |
| white-space: nowrap; | |
| } | |
| .data-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .data-table tr:hover td { | |
| background: rgba(255, 255, 255, 0.02); | |
| } | |
| .cell-null { | |
| color: var(--red); | |
| font-style: italic; | |
| } | |
| /* Empty state */ | |
| .empty-state { | |
| display: flex; | |
| flex-direction: column; | |
| align-items: center; | |
| justify-content: center; | |
| padding: 40px 24px; | |
| gap: 10px; | |
| color: var(--text-dim); | |
| text-align: center; | |
| } | |
| .empty-icon { | |
| font-size: 36px; | |
| opacity: 0.25; | |
| } | |
| .empty-title { | |
| font-family: var(--mono); | |
| font-size: 12px; | |
| letter-spacing: 2px; | |
| text-transform: uppercase; | |
| } | |
| .empty-sub { | |
| font-size: 12px; | |
| max-width: 280px; | |
| line-height: 1.6; | |
| } | |
| /* Bottom bar */ | |
| .bottom-bar { | |
| padding: 10px 28px; | |
| border-top: 1px solid var(--border); | |
| background: var(--bg2); | |
| display: flex; | |
| align-items: center; | |
| gap: 20px; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| color: var(--text-dim); | |
| grid-column: 1 / -1; | |
| flex-wrap: wrap; | |
| } | |
| .bottom-stat { | |
| display: flex; | |
| gap: 6px; | |
| } | |
| .bottom-stat span:last-child { | |
| color: var(--text); | |
| } | |
| .dl-btn { | |
| margin-left: auto; | |
| font-family: var(--mono); | |
| font-size: 10px; | |
| padding: 5px 14px; | |
| border-radius: 4px; | |
| border: 1px solid var(--green-dim); | |
| background: transparent; | |
| color: var(--green); | |
| cursor: pointer; | |
| letter-spacing: 1px; | |
| transition: all 0.2s; | |
| } | |
| .dl-btn:hover { | |
| border-color: var(--green); | |
| box-shadow: 0 0 10px rgba(0, 229, 160, 0.2); | |
| } | |
| .dl-btn:disabled { | |
| opacity: 0.3; | |
| cursor: not-allowed; | |
| } | |
| ::-webkit-scrollbar { | |
| width: 5px; | |
| height: 5px; | |
| } | |
| ::-webkit-scrollbar-track { | |
| background: var(--bg); | |
| } | |
| ::-webkit-scrollbar-thumb { | |
| background: var(--border); | |
| border-radius: 3px; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <!-- Header --> | |
| <header> | |
| <div class="logo"> | |
| <div class="logo-pulse" id="logo-pulse"></div> | |
| <div> | |
| <div class="logo-text">DATAMEDIC</div> | |
| <div class="logo-sub">AI Data Quality Monitor Β· OpenEnv</div> | |
| </div> | |
| </div> | |
| <span class="status-pill idle" id="status-pill">IDLE</span> | |
| </header> | |
| <!-- Controls --> | |
| <div class="controls"> | |
| <span class="ctrl-label">Select Task:</span> | |
| <button class="task-btn active" data-task="1" onclick="selectTask(1)">TASK 1 Β· Easy</button> | |
| <button class="task-btn" data-task="2" onclick="selectTask(2)">TASK 2 Β· Medium</button> | |
| <button class="task-btn" data-task="3" onclick="selectTask(3)">TASK 3 Β· Hard</button> | |
| <button class="task-btn" data-task="4" onclick="selectTask(4)">TASK 4 Β· Expert</button> | |
| <div class="sep"></div> | |
| <button class="reset-btn" id="reset-btn" onclick="resetEnv()">RESET EPISODE</button> | |
| <button class="run-btn" id="run-btn" onclick="runAgent()">RUN DEMO AGENT</button> | |
| <span class="run-hint">rule-based Β· follows plan field</span> | |
| </div> | |
| <!-- Main --> | |
| <div class="main"> | |
| <!-- LEFT: Vitals --> | |
| <div class="vitals-panel"> | |
| <div class="panel-title">Patient Vitals</div> | |
| <div class="score-ring-wrap"> | |
| <div class="ring-container"> | |
| <svg viewBox="0 0 130 130"> | |
| <circle class="ring-bg" cx="65" cy="65" r="52" /> | |
| <circle class="ring-fill" cx="65" cy="65" r="52" id="ring-fill" /> | |
| </svg> | |
| <div class="ring-text"> | |
| <div class="ring-score" id="ring-score">--</div> | |
| <div class="ring-label">Health Score</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="vital-grid"> | |
| <div class="vital-card"> | |
| <div class="vital-name">Step</div> | |
| <div class="vital-value blue" id="v-step">--</div> | |
| <div class="vital-sub" id="v-maxstep">of --</div> | |
| </div> | |
| <div class="vital-card"> | |
| <div class="vital-name">Reward</div> | |
| <div class="vital-value green" id="v-reward">--</div> | |
| <div class="vital-sub">last delta</div> | |
| </div> | |
| <div class="vital-card"> | |
| <div class="vital-name">Nulls</div> | |
| <div class="vital-value amber" id="v-nulls">--</div> | |
| <div class="vital-sub">missing cells</div> | |
| </div> | |
| <div class="vital-card"> | |
| <div class="vital-name">Dupes</div> | |
| <div class="vital-value amber" id="v-dupes">--</div> | |
| <div class="vital-sub">duplicate rows</div> | |
| </div> | |
| </div> | |
| <div class="panel-title">DQ Dimensions</div> | |
| <div class="dq-bars"> | |
| <div class="dq-row"> | |
| <div class="dq-header"> | |
| <span class="dq-name">Completeness</span> | |
| <span class="dq-val" id="dq-completeness" style="color:var(--green)">--</span> | |
| </div> | |
| <div class="dq-bar-bg"> | |
| <div class="dq-bar-fill" id="bar-completeness" style="width:0%;background:var(--green)"></div> | |
| </div> | |
| </div> | |
| <div class="dq-row"> | |
| <div class="dq-header"> | |
| <span class="dq-name">Uniqueness</span> | |
| <span class="dq-val" id="dq-uniqueness" style="color:var(--blue)">--</span> | |
| </div> | |
| <div class="dq-bar-bg"> | |
| <div class="dq-bar-fill" id="bar-uniqueness" style="width:0%;background:var(--blue)"></div> | |
| </div> | |
| </div> | |
| <div class="dq-row"> | |
| <div class="dq-header"> | |
| <span class="dq-name">Validity</span> | |
| <span class="dq-val" id="dq-validity" style="color:var(--amber)">--</span> | |
| </div> | |
| <div class="dq-bar-bg"> | |
| <div class="dq-bar-fill" id="bar-validity" style="width:0%;background:var(--amber)"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- RIGHT: Content --> | |
| <div class="content-area"> | |
| <div class="chart-section"> | |
| <div class="panel-title">Health Score Trajectory</div> | |
| <div class="chart-wrap"> | |
| <svg id="score-chart" preserveAspectRatio="none"> | |
| <defs> | |
| <linearGradient id="chartGrad" x1="0" y1="0" x2="0" y2="1"> | |
| <stop offset="0%" stop-color="#00e5a0" stop-opacity="0.25" /> | |
| <stop offset="100%" stop-color="#00e5a0" stop-opacity="0" /> | |
| </linearGradient> | |
| </defs> | |
| <path id="chart-area" fill="url(#chartGrad)" d="" /> | |
| <path id="chart-line" fill="none" stroke="#00e5a0" stroke-width="2" stroke-linecap="round" | |
| stroke-linejoin="round" d="" style="filter:drop-shadow(0 0 3px #00e5a0)" /> | |
| <text x="50%" y="50%" text-anchor="middle" dominant-baseline="middle" fill="#4a6a8a" | |
| font-size="11" id="chart-empty-msg" font-family="Courier New, monospace"> | |
| Run demo agent to see score trajectory | |
| </text> | |
| </svg> | |
| </div> | |
| </div> | |
| <div class="plan-section"> | |
| <div class="panel-title">Agent Treatment Plan <span style="color:var(--amber);font-size:9px">(next | |
| recommended actions)</span></div> | |
| <div class="plan-items" id="plan-items"> | |
| <div style="color:var(--text-dim);font-size:11px;font-family:var(--mono);padding:4px 0"> | |
| Awaiting diagnosis... | |
| </div> | |
| </div> | |
| </div> | |
| <div class="thought-section"> | |
| <div class="panel-title">Agent Operation Log <span | |
| style="color:var(--text-dim);font-size:9px">(actions taken + results)</span></div> | |
| <div class="thought-stream" id="thought-stream"> | |
| <div class="empty-state" style="padding:16px"> | |
| <div class="empty-sub">Actions will appear here as the demo agent runs</div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="preview-section"> | |
| <div class="panel-title">Dataset Preview <span style="color:var(--text-dim);font-size:9px">(first | |
| 10 rows Β· NULL shown in red)</span></div> | |
| <div class="data-table-wrap" id="table-wrap"> | |
| <div class="empty-state"> | |
| <div class="empty-icon">[?]</div> | |
| <div class="empty-title">No Dataset Loaded</div> | |
| <div class="empty-sub">Select a task β dataset loads automatically</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Bottom bar --> | |
| <div class="bottom-bar"> | |
| <div class="bottom-stat"><span>Episode:</span><span id="b-episode">--</span></div> | |
| <div class="bottom-stat"><span>Task:</span><span id="b-task">--</span></div> | |
| <div class="bottom-stat"><span>Errors Left:</span><span id="b-errors">--</span></div> | |
| <div class="bottom-stat"><span>Shape:</span><span id="b-shape">--</span></div> | |
| <button class="dl-btn" id="dl-btn" disabled onclick="downloadCSV()">EXPORT CSV</button> | |
| </div> | |
| </div> | |
| <script> | |
| const BASE = ''; | |
| let selectedTask = 1; | |
| let scores = []; | |
| let isRunning = false; | |
| const TASK_LABELS = { | |
| 1: 'Task 1 - Fill Missing Values', | |
| 2: 'Task 2 - Fix Formats + Duplicates', | |
| 3: 'Task 3 - Full Pipeline', | |
| 4: 'Task 4 - Multi-Source Merge' | |
| }; | |
| // ββ Task selection: switch + auto-reset ββββββββββββββββββββββββββ | |
| function selectTask(n) { | |
| if (isRunning) return; | |
| selectedTask = n; | |
| document.querySelectorAll('.task-btn').forEach(b => b.classList.remove('active')); | |
| document.querySelector('[data-task="' + n + '"]').classList.add('active'); | |
| resetEnv(); // <-- auto-reset when task changes | |
| } | |
| // ββ Reset ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function resetEnv() { | |
| if (isRunning) return; | |
| setButtons(false); | |
| // Immediately update task label and dim ring while loading | |
| document.getElementById('b-task').textContent = TASK_LABELS[selectedTask] || 'Task ' + selectedTask; | |
| document.getElementById('ring-score').textContent = '...'; | |
| document.getElementById('ring-fill').style.strokeDashoffset = 326.73; | |
| try { | |
| const r = await fetch(BASE + '/reset', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ task_id: selectedTask }) | |
| }); | |
| if (!r.ok) throw new Error('Reset failed: ' + r.status); | |
| const data = await r.json(); | |
| scores = [data.observation.current_score]; | |
| updateUI(data.observation, null); | |
| clearThoughts(); | |
| updateChart(); | |
| addThought(0, 'Episode started - Task ' + selectedTask, data.observation.message, null); | |
| document.getElementById('dl-btn').disabled = false; | |
| setStatus('idle'); | |
| document.getElementById('b-task').textContent = TASK_LABELS[selectedTask] || 'Task ' + selectedTask; | |
| } catch (e) { | |
| addThought('!', 'Error', e.message, null); | |
| console.error(e); | |
| } | |
| setButtons(true); | |
| } | |
| // ββ Run demo agent βββββββββββββββββββββββββββββββββββββββββββββββ | |
| async function runAgent() { | |
| if (isRunning) return; | |
| isRunning = true; | |
| setButtons(false); | |
| setStatus('running'); | |
| // Fresh reset first | |
| try { | |
| const initR = await fetch(BASE + '/reset', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ task_id: selectedTask }) | |
| }); | |
| const initData = await initR.json(); | |
| let obs = initData.observation; | |
| scores = [obs.current_score]; | |
| clearThoughts(); | |
| updateUI(obs, null); | |
| updateChart(); | |
| addThought(0, 'Demo agent started', obs.message, null); | |
| const MAX = 50; | |
| let step = 0; | |
| while (!obs.done && step < MAX) { | |
| await sleep(700); | |
| const action = pickAction(obs); | |
| if (!action) { | |
| addThought('--', 'Agent halted', 'No more actions available from plan', null); | |
| break; | |
| } | |
| step++; | |
| const r = await fetch(BASE + '/step', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify(action) | |
| }); | |
| const data = await r.json(); | |
| obs = data.observation; | |
| scores.push(obs.current_score); | |
| updateUI(obs, data.reward); | |
| updateChart(); | |
| addThought(step, JSON.stringify(action), obs.message, data.reward); | |
| const ts = document.getElementById('thought-stream'); | |
| ts.scrollTop = ts.scrollHeight; | |
| } | |
| const done = obs.current_score >= 0.95; | |
| setStatus(done ? 'done' : 'idle'); | |
| if (done) { | |
| addThought('OK', 'Cleaning complete!', | |
| 'Final score: ' + (obs.current_score * 100).toFixed(1) + '%', null); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| addThought('!', 'Error during agent run', e.message, null); | |
| setStatus('idle'); | |
| } | |
| isRunning = false; | |
| setButtons(true); | |
| } | |
| // ββ Rule-based action picker (follows plan field) ββββββββββββββββ | |
| function pickAction(obs) { | |
| if (obs.plan && obs.plan.length > 0) { | |
| const p = obs.plan[0]; | |
| if (p.startsWith('align_schema')) | |
| return { operation: 'align_schema' }; | |
| if (p.startsWith('merge_sources')) | |
| return { operation: 'merge_sources' }; | |
| if (p.startsWith('drop_duplicates')) | |
| return { operation: 'drop_duplicates' }; | |
| const fillM = p.match(/fill_missing on "([^"]+)".*?(median|mode|mean)/); | |
| if (fillM) | |
| return { operation: 'fill_missing', column: fillM[1], params: { strategy: fillM[2] } }; | |
| const fmtM = p.match(/fix_format on "([^"]+)"/); | |
| if (fmtM) | |
| return { operation: 'fix_format', column: fmtM[1] }; | |
| const outM = p.match(/drop_outliers on "([^"]+)"/); | |
| if (outM) | |
| return { operation: 'drop_outliers', column: outM[1] }; | |
| } | |
| // Fallback: scan missing counts directly | |
| const missing = obs.missing_counts || {}; | |
| for (const [col, cnt] of Object.entries(missing)) { | |
| if (cnt > 0) { | |
| const cat = ['department', 'country', 'email', 'name', 'category'].includes(col); | |
| return { operation: 'fill_missing', column: col, params: { strategy: cat ? 'mode' : 'median' } }; | |
| } | |
| } | |
| if (obs.duplicate_count > 0) | |
| return { operation: 'drop_duplicates' }; | |
| return null; | |
| } | |
| // ββ UI update ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateUI(obs, reward) { | |
| const pct = obs.current_score; | |
| const CIRCUM = 326.73; // exact: 2 * pi * 52 | |
| // Ring β minimum 3% arc so ring is never invisible at very low scores | |
| const displayPct = Math.max(pct, 0.03); | |
| document.getElementById('ring-fill').style.strokeDashoffset = CIRCUM * (1 - displayPct); | |
| // Score text β show raw value accurately | |
| const scoreText = pct < 0.1 | |
| ? (pct * 100).toFixed(1) + '%' // e.g. "4.3%" | |
| : (pct * 100).toFixed(1) + '%'; // e.g. "87.5%" | |
| document.getElementById('ring-score').textContent = scoreText; | |
| // Color ring by health | |
| const col = pct >= 0.85 ? '#00e5a0' : pct >= 0.5 ? '#f5a623' : '#ff4d6d'; | |
| const rf = document.getElementById('ring-fill'); | |
| rf.style.stroke = col; | |
| rf.style.filter = 'drop-shadow(0 0 5px ' + col + ')'; | |
| document.getElementById('ring-score').style.color = col; | |
| // Stats | |
| document.getElementById('v-step').textContent = obs.step_count; | |
| document.getElementById('v-maxstep').textContent = 'of ' + (obs.step_count + 20); | |
| if (reward !== null) { | |
| const rv = document.getElementById('v-reward'); | |
| rv.textContent = (reward >= 0 ? '+' : '') + reward.toFixed(4); | |
| rv.className = 'vital-value ' + (reward >= 0 ? 'green' : 'red'); | |
| } | |
| const nullTotal = Object.values(obs.missing_counts || {}).reduce(function (a, b) { return a + b; }, 0); | |
| const vn = document.getElementById('v-nulls'); | |
| vn.textContent = nullTotal; | |
| vn.className = 'vital-value ' + (nullTotal === 0 ? 'green' : 'amber'); | |
| const vd = document.getElementById('v-dupes'); | |
| vd.textContent = obs.duplicate_count; | |
| vd.className = 'vital-value ' + (obs.duplicate_count === 0 ? 'green' : 'amber'); | |
| // DQ bars | |
| if (obs.dq_metrics) { | |
| setDQBar('completeness', obs.dq_metrics.completeness_pct, 'var(--green)'); | |
| setDQBar('uniqueness', obs.dq_metrics.uniqueness_pct, 'var(--blue)'); | |
| setDQBar('validity', obs.dq_metrics.validity_pct, 'var(--amber)'); | |
| } | |
| // Plan | |
| const planEl = document.getElementById('plan-items'); | |
| if (obs.plan && obs.plan.length > 0) { | |
| planEl.innerHTML = obs.plan.map(function (p, i) { | |
| return '<div class="plan-item">' + | |
| '<div class="plan-num">' + (i + 1) + '</div>' + | |
| '<span style="color:var(--text)">' + p + '</span>' + | |
| '</div>'; | |
| }).join(''); | |
| } else if (obs.done) { | |
| planEl.innerHTML = '<div style="color:var(--green);font-family:var(--mono);font-size:11px;padding:4px 0">Dataset fully cleaned</div>'; | |
| } else { | |
| planEl.innerHTML = '<div style="color:var(--text-dim);font-family:var(--mono);font-size:11px;padding:4px 0">No further actions needed</div>'; | |
| } | |
| // Table | |
| if (obs.data_preview) renderTable(obs.data_preview); | |
| // Bottom bar | |
| document.getElementById('b-shape').textContent = obs.data_shape[0] + ' x ' + obs.data_shape[1]; | |
| } | |
| function setDQBar(name, val, color) { | |
| document.getElementById('dq-' + name).textContent = val.toFixed(1) + '%'; | |
| document.getElementById('bar-' + name).style.width = Math.min(val, 100) + '%'; | |
| document.getElementById('bar-' + name).style.background = color; | |
| } | |
| // ββ Chart ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function updateChart() { | |
| const svg = document.getElementById('score-chart'); | |
| const W = svg.clientWidth || 600; | |
| const H = svg.clientHeight || 90; | |
| const pad = 6; | |
| if (scores.length < 2) return; | |
| document.getElementById('chart-empty-msg').style.display = 'none'; | |
| const xs = scores.map(function (_, i) { return pad + (i / (scores.length - 1)) * (W - 2 * pad); }); | |
| const ys = scores.map(function (s) { return (H - pad) - s * (H - 2 * pad); }); | |
| const pts = xs.map(function (x, i) { return x + ',' + ys[i]; }).join(' L '); | |
| document.getElementById('chart-line').setAttribute('d', 'M ' + pts); | |
| document.getElementById('chart-area').setAttribute('d', | |
| 'M ' + xs[0] + ',' + H + ' L ' + pts + ' L ' + xs[xs.length - 1] + ',' + H + ' Z' | |
| ); | |
| } | |
| // ββ Table ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function renderTable(csv) { | |
| const lines = csv.trim().split('\n'); | |
| if (lines.length < 2) return; | |
| const headers = lines[0].split(','); | |
| const rows = lines.slice(1, 11).map(function (l) { return l.split(','); }); | |
| var html = '<table class="data-table"><thead><tr>' + | |
| headers.map(function (h) { return '<th>' + h.trim() + '</th>'; }).join('') + | |
| '</tr></thead><tbody>'; | |
| rows.forEach(function (row) { | |
| html += '<tr>' + row.map(function (cell) { | |
| var v = cell.trim(); | |
| var empty = v === '' || v.toLowerCase() === 'nan' || v.toLowerCase() === 'none'; | |
| return '<td class="' + (empty ? 'cell-null' : '') + '">' + (empty ? 'NULL' : v) + '</td>'; | |
| }).join('') + '</tr>'; | |
| }); | |
| html += '</tbody></table>'; | |
| document.getElementById('table-wrap').innerHTML = html; | |
| } | |
| // ββ Thought stream βββββββββββββββββββββββββββββββββββββββββββββββ | |
| function clearThoughts() { | |
| document.getElementById('thought-stream').innerHTML = ''; | |
| } | |
| function addThought(step, action, result, reward) { | |
| const ts = document.getElementById('thought-stream'); | |
| const rewardHtml = reward !== null | |
| ? '<div class="thought-reward ' + (reward >= 0 ? 'reward-pos' : 'reward-neg') + '">' + | |
| (reward >= 0 ? '+' : '') + reward.toFixed(4) + '</div>' | |
| : ''; | |
| var el = document.createElement('div'); | |
| el.className = 'thought-item'; | |
| el.innerHTML = | |
| '<div class="thought-step">S' + step + '</div>' + | |
| '<div class="thought-body">' + | |
| '<div class="thought-action">' + action + '</div>' + | |
| '<div class="thought-result">' + result + '</div>' + | |
| rewardHtml + | |
| '</div>'; | |
| ts.appendChild(el); | |
| } | |
| // ββ Helpers ββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| function setStatus(s) { | |
| const el = document.getElementById('status-pill'); | |
| el.className = 'status-pill ' + s; | |
| el.textContent = s.toUpperCase(); | |
| } | |
| function setButtons(enabled) { | |
| document.getElementById('run-btn').disabled = !enabled; | |
| document.getElementById('reset-btn').disabled = !enabled; | |
| } | |
| async function downloadCSV() { | |
| try { | |
| const r = await fetch(BASE + '/export'); | |
| const text = await r.text(); | |
| const blob = new Blob([text], { type: 'text/csv' }); | |
| const a = document.createElement('a'); | |
| a.href = URL.createObjectURL(blob); | |
| a.download = 'cleaned_task' + selectedTask + '.csv'; | |
| a.click(); | |
| } catch (e) { | |
| console.error('Export failed:', e); | |
| } | |
| } | |
| function sleep(ms) { return new Promise(function (r) { setTimeout(r, ms); }); } | |
| // Auto-load Task 1 on open | |
| window.addEventListener('load', function () { resetEnv(); }); | |
| </script> | |
| </body> | |
| </html> |