Spaces:
Sleeping
Sleeping
Commit Β·
7abab00
1
Parent(s): 87f5919
removing unnescearry things
Browse files- README.md +66 -139
- app/dashboard.py +209 -243
- dashboard.html +913 -0
README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
| 2 |
title: Trainx
|
| 3 |
emoji: π
|
| 4 |
colorFrom: red
|
| 5 |
-
colorTo:
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
license: apache-2.0
|
|
@@ -12,185 +12,116 @@ license: apache-2.0
|
|
| 12 |
|
| 13 |
**An RL environment for training LLM agents to write production-ready, secure Python code.**
|
| 14 |
|
| 15 |
-
Built for the **Meta Γ PyTorch OpenEnv Hackathon 2026** by Vishal Dhakad (`vishaldhakad`).
|
| 16 |
-
|
| 17 |
---
|
| 18 |
|
| 19 |
## The Problem
|
| 20 |
|
| 21 |
-
Studies show **12β65% of LLM-generated code contains security vulnerabilities**
|
| 22 |
-
|
| 23 |
-
Every existing RL environment trains agents to write code that **WORKS**. None train agents to write code that is **SAFE, CONSISTENT, and PRODUCTION-READY**.
|
| 24 |
|
| 25 |
-
SecureCodeEnv
|
| 26 |
|
| 27 |
---
|
| 28 |
|
| 29 |
-
## What Makes This Environment
|
| 30 |
|
| 31 |
-
| Feature | SecureCodeEnv |
|
| 32 |
|---|---|---|
|
| 33 |
-
| Dynamic adversarial grading | β
|
| 34 |
-
| CodeGraph memory | β
|
| 35 |
| CWE-grounded tasks | β
9 tasks, 12+ CWE IDs | β Generic correctness |
|
| 36 |
-
|
|
| 37 |
| Anti-reward-hacking | β
Seeded random payloads | β Fixed test cases |
|
| 38 |
|
| 39 |
-
### CodeGraph Memory System
|
| 40 |
-
|
| 41 |
-
The environment maintains a `CodeGraph` β a structured in-memory database of every component the agent has written in the current episode. When the agent writes `auth/validator.py` in `snake_case`, and then submits `auth/middleware.py` in `camelCase`, the consistency grader penalizes the drift. No other RL environment does this.
|
| 42 |
-
|
| 43 |
-
### Dynamic Adversarial Attack Grading
|
| 44 |
-
|
| 45 |
-
We don't just scan for vulnerability patterns β we **fire real attacks** at the agent's code:
|
| 46 |
-
- SQL injection payloads (UNION SELECT, OR 1=1, stacked queries)
|
| 47 |
-
- Path traversal payloads (`../../etc/passwd`, URL-encoded variants)
|
| 48 |
-
- JWT bypass attacks (`alg: none`, expired tokens, tampered payloads)
|
| 49 |
-
- XSS payloads (`<script>`, `onerror=`, template injection)
|
| 50 |
-
|
| 51 |
-
Payloads are randomized per episode using a seed. The agent **cannot memorize** specific strings.
|
| 52 |
-
|
| 53 |
---
|
| 54 |
|
| 55 |
-
## Reward System
|
| 56 |
|
| 57 |
| Dimension | Weight | Tool | What It Measures |
|
| 58 |
|---|---|---|---|
|
| 59 |
-
|
|
| 60 |
-
|
|
| 61 |
-
|
|
| 62 |
-
|
|
| 63 |
-
|
|
| 64 |
-
|
|
| 65 |
-
|
|
| 66 |
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
## Quick Start
|
| 70 |
-
|
| 71 |
-
```python
|
| 72 |
-
import requests
|
| 73 |
-
|
| 74 |
-
ENV_URL = "https://vishaldhakad-securecodeenv.hf.space"
|
| 75 |
-
|
| 76 |
-
# 1. Start episode
|
| 77 |
-
episode = requests.post(f"{ENV_URL}/reset", json={"difficulty": "medium"}).json()
|
| 78 |
-
sid = episode["session_id"]
|
| 79 |
-
print(episode["problem_statement"])
|
| 80 |
-
|
| 81 |
-
# 2. Submit code
|
| 82 |
-
result = requests.post(f"{ENV_URL}/step", json={
|
| 83 |
-
"session_id": sid,
|
| 84 |
-
"code": "def build_user_query(username, role):\n return ('SELECT * FROM users WHERE username = %s', (username,))",
|
| 85 |
-
"filename": "solution.py",
|
| 86 |
-
}).json()
|
| 87 |
-
|
| 88 |
-
print(f"Reward: {result['total_reward']:.3f}")
|
| 89 |
-
print(f"Scores: {result['scores']}")
|
| 90 |
-
print(f"Feedback: {result['feedback']['summary']}")
|
| 91 |
-
```
|
| 92 |
|
| 93 |
---
|
| 94 |
|
| 95 |
## Tasks β 9 Tasks Across 3 Difficulty Levels
|
| 96 |
|
| 97 |
### Easy
|
| 98 |
-
| Task | CWE Targets |
|
| 99 |
-
|---|---|
|
| 100 |
-
| Password Validator | CWE-916, CWE-521 |
|
| 101 |
-
| Input Sanitizer | CWE-20, CWE-116 |
|
| 102 |
-
| Token Generator | CWE-338, CWE-330 |
|
| 103 |
|
| 104 |
### Medium
|
| 105 |
-
| Task | CWE Targets |
|
| 106 |
-
|---|---|
|
| 107 |
-
| SQL Query Builder | CWE-89 |
|
| 108 |
-
| File Path Handler | CWE-22 |
|
| 109 |
-
| Rate Limiter | CWE-770, CWE-400 |
|
| 110 |
|
| 111 |
### Hard
|
| 112 |
-
| Task | CWE Targets |
|
| 113 |
-
|---|---|
|
| 114 |
-
| File Upload Handler | CWE-22, CWE-434 |
|
| 115 |
-
| JWT Validator | CWE-347, CWE-613 |
|
| 116 |
-
| Auth Middleware | CWE-287, CWE-352 |
|
| 117 |
|
| 118 |
---
|
| 119 |
|
| 120 |
-
##
|
| 121 |
|
| 122 |
-
|
| 123 |
-
|
| 124 |
|
| 125 |
-
|
| 126 |
-
```json
|
| 127 |
-
{ "difficulty": "medium" }
|
| 128 |
-
```
|
| 129 |
|
| 130 |
-
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
|
| 134 |
-
"task_id": "medium_sql_query_builder",
|
| 135 |
-
"problem_statement": "Write a Python function...",
|
| 136 |
-
"difficulty": "medium",
|
| 137 |
-
"cwe_targets": ["CWE-89", "CWE-20"],
|
| 138 |
-
"codegraph": { "components": {}, "conventions": {} },
|
| 139 |
-
"starter_code": "def build_user_query(...):"
|
| 140 |
-
}
|
| 141 |
-
```
|
| 142 |
|
| 143 |
-
#
|
| 144 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
{
|
| 149 |
-
"session_id": "uuid",
|
| 150 |
-
"code": "def build_user_query(username: str, role: str) -> tuple: ...",
|
| 151 |
-
"filename": "src/db/queries.py"
|
| 152 |
-
}
|
| 153 |
```
|
| 154 |
|
| 155 |
-
|
| 156 |
-
```json
|
| 157 |
-
{
|
| 158 |
-
"total_reward": 0.847,
|
| 159 |
-
"scores": {
|
| 160 |
-
"correctness": 1.0,
|
| 161 |
-
"attack_resist": 0.875,
|
| 162 |
-
"static_security": 0.9,
|
| 163 |
-
"consistency": 1.0,
|
| 164 |
-
"performance": 0.72,
|
| 165 |
-
"documentation": 0.75,
|
| 166 |
-
"code_structure": 0.8
|
| 167 |
-
},
|
| 168 |
-
"feedback": { "summary": "π‘ Good submission β improve: performance" },
|
| 169 |
-
"codegraph": { ... },
|
| 170 |
-
"done": false,
|
| 171 |
-
"step_count": 1
|
| 172 |
-
}
|
| 173 |
-
```
|
| 174 |
|
| 175 |
-
##
|
| 176 |
-
Get current episode state without advancing.
|
| 177 |
|
| 178 |
-
|
| 179 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 180 |
|
| 181 |
---
|
| 182 |
|
| 183 |
-
## Setup
|
| 184 |
|
| 185 |
```bash
|
| 186 |
-
git clone https://huggingface.co/spaces/vishaldhakad/SecureCodeEnv
|
| 187 |
-
cd SecureCodeEnv
|
| 188 |
-
|
| 189 |
# Docker (recommended)
|
| 190 |
docker build -t secure-code-env .
|
| 191 |
docker run -p 7860:7860 secure-code-env
|
| 192 |
|
| 193 |
-
#
|
| 194 |
pip install -r requirements.txt
|
| 195 |
uvicorn app.main:app --host 0.0.0.0 --port 7860
|
| 196 |
```
|
|
@@ -200,12 +131,12 @@ uvicorn app.main:app --host 0.0.0.0 --port 7860
|
|
| 200 |
```bash
|
| 201 |
export API_BASE_URL=https://api.openai.com/v1
|
| 202 |
export MODEL_NAME=gpt-4o-mini
|
| 203 |
-
export HF_TOKEN=
|
| 204 |
export ENV_URL=http://localhost:7860
|
| 205 |
python inference.py
|
| 206 |
```
|
| 207 |
|
| 208 |
-
##
|
| 209 |
|
| 210 |
```bash
|
| 211 |
python validate.py --url http://localhost:7860
|
|
@@ -217,11 +148,7 @@ python validate.py --url http://localhost:7860
|
|
| 217 |
|
| 218 |
| Variable | Required | Description |
|
| 219 |
|---|---|---|
|
| 220 |
-
| `API_BASE_URL` | Yes | LLM API endpoint
|
| 221 |
-
| `MODEL_NAME` | Yes | Model identifier
|
| 222 |
-
| `HF_TOKEN` | Yes |
|
| 223 |
| `ENV_URL` | No | Override environment URL (default: localhost:7860) |
|
| 224 |
-
|
| 225 |
-
---
|
| 226 |
-
|
| 227 |
-
*SecureCodeEnv v2.0 Β· Meta Γ PyTorch OpenEnv Hackathon 2026 Β· Vishal Dhakad*
|
|
|
|
| 2 |
title: Trainx
|
| 3 |
emoji: π
|
| 4 |
colorFrom: red
|
| 5 |
+
colorTo: orange
|
| 6 |
sdk: docker
|
| 7 |
pinned: true
|
| 8 |
license: apache-2.0
|
|
|
|
| 12 |
|
| 13 |
**An RL environment for training LLM agents to write production-ready, secure Python code.**
|
| 14 |
|
|
|
|
|
|
|
| 15 |
---
|
| 16 |
|
| 17 |
## The Problem
|
| 18 |
|
| 19 |
+
Studies show **12β65% of LLM-generated code contains security vulnerabilities**. Secure-pass@1 rates remain below 12% for all frontier models even when functional pass@1 exceeds 50%.
|
|
|
|
|
|
|
| 20 |
|
| 21 |
+
Every existing RL environment trains agents to write code that **works**. None train agents to write code that is **safe, consistent, and production-ready**. SecureCodeEnv closes that gap.
|
| 22 |
|
| 23 |
---
|
| 24 |
|
| 25 |
+
## What Makes This Environment Different
|
| 26 |
|
| 27 |
+
| Feature | SecureCodeEnv | Typical RL Code Envs |
|
| 28 |
|---|---|---|
|
| 29 |
+
| Dynamic adversarial grading | β
Real attacks fired per episode | β Static patterns only |
|
| 30 |
+
| CodeGraph memory | β
Cross-step convention tracking | β Single-function only |
|
| 31 |
| CWE-grounded tasks | β
9 tasks, 12+ CWE IDs | β Generic correctness |
|
| 32 |
+
| Security gate on done | β
Attack + static thresholds | β Pass/fail only |
|
| 33 |
| Anti-reward-hacking | β
Seeded random payloads | β Fixed test cases |
|
| 34 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 35 |
---
|
| 36 |
|
| 37 |
+
## Reward System β 7 Dimensions
|
| 38 |
|
| 39 |
| Dimension | Weight | Tool | What It Measures |
|
| 40 |
|---|---|---|---|
|
| 41 |
+
| correctness | 25% | Custom test runner | Test cases passed |
|
| 42 |
+
| attack_resist | 25% | Dynamic harness | Real attack payloads blocked |
|
| 43 |
+
| static_security | 20% | bandit + AST | CWE-mapped vulnerability patterns |
|
| 44 |
+
| consistency | 10% | CodeGraph | Convention adherence across steps |
|
| 45 |
+
| performance | 8% | timeit | Speed vs naive/optimal baselines |
|
| 46 |
+
| documentation | 7% | AST | Docstring + type hint coverage |
|
| 47 |
+
| code_structure | 5% | AST | Clean code (no bare print/except) |
|
| 48 |
|
| 49 |
+
**Security gate:** episode cannot complete unless `attack_resist β₯ 0.75` AND `static_security β₯ 0.70` AND `correctness β₯ 0.80`.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 50 |
|
| 51 |
---
|
| 52 |
|
| 53 |
## Tasks β 9 Tasks Across 3 Difficulty Levels
|
| 54 |
|
| 55 |
### Easy
|
| 56 |
+
| Task | CWE Targets |
|
| 57 |
+
|---|---|
|
| 58 |
+
| Password Validator | CWE-916, CWE-521 |
|
| 59 |
+
| Input Sanitizer | CWE-20, CWE-116 |
|
| 60 |
+
| Token Generator | CWE-338, CWE-330 |
|
| 61 |
|
| 62 |
### Medium
|
| 63 |
+
| Task | CWE Targets |
|
| 64 |
+
|---|---|
|
| 65 |
+
| SQL Query Builder | CWE-89 |
|
| 66 |
+
| File Path Handler | CWE-22 |
|
| 67 |
+
| Rate Limiter | CWE-770, CWE-400 |
|
| 68 |
|
| 69 |
### Hard
|
| 70 |
+
| Task | CWE Targets |
|
| 71 |
+
|---|---|
|
| 72 |
+
| File Upload Handler | CWE-22, CWE-434 |
|
| 73 |
+
| JWT Validator | CWE-347, CWE-613 |
|
| 74 |
+
| Auth Middleware | CWE-287, CWE-352 |
|
| 75 |
|
| 76 |
---
|
| 77 |
|
| 78 |
+
## Quick Start
|
| 79 |
|
| 80 |
+
```python
|
| 81 |
+
import requests
|
| 82 |
|
| 83 |
+
BASE = "http://localhost:7860"
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
+
# Start episode
|
| 86 |
+
ep = requests.post(f"{BASE}/reset", json={"difficulty": "medium"}).json()
|
| 87 |
+
sid = ep["session_id"]
|
| 88 |
+
print(ep["problem_statement"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 89 |
|
| 90 |
+
# Submit code
|
| 91 |
+
result = requests.post(f"{BASE}/step", json={
|
| 92 |
+
"session_id": sid,
|
| 93 |
+
"code": "def build_user_query(u, r):\n return ('SELECT * FROM users WHERE username=%s', (u,))",
|
| 94 |
+
"filename": "solution.py"
|
| 95 |
+
}).json()
|
| 96 |
|
| 97 |
+
print(f"reward={result['total_reward']:.3f}")
|
| 98 |
+
print(result["feedback"]["summary"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
```
|
| 100 |
|
| 101 |
+
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 102 |
|
| 103 |
+
## API Endpoints
|
|
|
|
| 104 |
|
| 105 |
+
| Method | Endpoint | Description |
|
| 106 |
+
|---|---|---|
|
| 107 |
+
| GET | /health | Health check |
|
| 108 |
+
| POST | /reset | Start new episode |
|
| 109 |
+
| POST | /step | Submit code for grading |
|
| 110 |
+
| GET | /state | Current episode state |
|
| 111 |
+
| GET | /tasks | List all tasks |
|
| 112 |
+
| GET | /tasks/{id} | Task detail + starter code |
|
| 113 |
+
| GET | /docs | Swagger UI |
|
| 114 |
|
| 115 |
---
|
| 116 |
|
| 117 |
+
## Setup
|
| 118 |
|
| 119 |
```bash
|
|
|
|
|
|
|
|
|
|
| 120 |
# Docker (recommended)
|
| 121 |
docker build -t secure-code-env .
|
| 122 |
docker run -p 7860:7860 secure-code-env
|
| 123 |
|
| 124 |
+
# Direct
|
| 125 |
pip install -r requirements.txt
|
| 126 |
uvicorn app.main:app --host 0.0.0.0 --port 7860
|
| 127 |
```
|
|
|
|
| 131 |
```bash
|
| 132 |
export API_BASE_URL=https://api.openai.com/v1
|
| 133 |
export MODEL_NAME=gpt-4o-mini
|
| 134 |
+
export HF_TOKEN=your_token
|
| 135 |
export ENV_URL=http://localhost:7860
|
| 136 |
python inference.py
|
| 137 |
```
|
| 138 |
|
| 139 |
+
## Pre-submission Validation
|
| 140 |
|
| 141 |
```bash
|
| 142 |
python validate.py --url http://localhost:7860
|
|
|
|
| 148 |
|
| 149 |
| Variable | Required | Description |
|
| 150 |
|---|---|---|
|
| 151 |
+
| `API_BASE_URL` | Yes (inference) | LLM API endpoint |
|
| 152 |
+
| `MODEL_NAME` | Yes (inference) | Model identifier |
|
| 153 |
+
| `HF_TOKEN` | Yes (inference) | API authentication token |
|
| 154 |
| `ENV_URL` | No | Override environment URL (default: localhost:7860) |
|
|
|
|
|
|
|
|
|
|
|
|
app/dashboard.py
CHANGED
|
@@ -20,7 +20,6 @@ DASHBOARD_HTML = r"""<!DOCTYPE html>
|
|
| 20 |
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans)}
|
| 21 |
body{display:flex;flex-direction:column;min-height:100vh}
|
| 22 |
|
| 23 |
-
/* grid bg */
|
| 24 |
body::before{content:'';position:fixed;inset:0;
|
| 25 |
background-image:linear-gradient(rgba(240,136,62,.025) 1px,transparent 1px),
|
| 26 |
linear-gradient(90deg,rgba(240,136,62,.025) 1px,transparent 1px);
|
|
@@ -49,7 +48,7 @@ header{position:sticky;top:0;z-index:200;background:rgba(7,9,13,.88);
|
|
| 49 |
padding:0 24px;gap:2px;position:sticky;top:52px;z-index:100}
|
| 50 |
.ntab{font-family:var(--mono);font-size:12px;padding:10px 16px;cursor:pointer;
|
| 51 |
border-bottom:2px solid transparent;color:var(--muted);transition:.15s;
|
| 52 |
-
background:none;border-top:none;border-left:none;border-right:none
|
| 53 |
.ntab:hover{color:var(--text)}
|
| 54 |
.ntab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
| 55 |
|
|
@@ -86,12 +85,10 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 86 |
.btn-green{background:var(--a3);color:#000}
|
| 87 |
.btn-green:hover{background:#6fe87a}
|
| 88 |
.btn-green:disabled{background:var(--s3);color:var(--muted);cursor:not-allowed}
|
| 89 |
-
.btn-danger{background:transparent;color:var(--danger);border:1px solid rgba(255,123,114,.3)}
|
| 90 |
-
.btn-danger:hover{background:rgba(255,123,114,.1)}
|
| 91 |
|
| 92 |
/* ββ task display ββ */
|
| 93 |
.task-box{background:var(--s2);border:1px solid var(--border);border-radius:6px;padding:14px;
|
| 94 |
-
font-size:13px;line-height:1.7;color:var(--text);white-space:pre-wrap;max-height:
|
| 95 |
overflow-y:auto;font-family:var(--mono)}
|
| 96 |
.task-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
| 97 |
.cwe{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:4px;
|
|
@@ -124,22 +121,19 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 124 |
|
| 125 |
/* ββ reward display ββ */
|
| 126 |
.reward-big{text-align:center;padding:20px 14px}
|
| 127 |
-
.reward-number{font-family:var(--mono);font-size:52px;font-weight:700;line-height:1;
|
| 128 |
-
transition:all .4s ease}
|
| 129 |
.reward-label{font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:4px}
|
| 130 |
.reward-bar-bg{height:6px;background:var(--s3);border-radius:99px;margin:12px 0}
|
| 131 |
.reward-bar{height:6px;border-radius:99px;background:var(--accent);transition:width .6s ease;width:0%}
|
| 132 |
|
| 133 |
/* ββ score breakdown ββ */
|
| 134 |
-
.score-row{display:flex;align-items:center;gap:8px;padding:5px 0;
|
| 135 |
-
border-bottom:1px solid var(--border);font-size:12px}
|
| 136 |
.score-row:last-child{border:none}
|
| 137 |
.score-dim{flex:1;color:var(--muted);font-family:var(--mono)}
|
| 138 |
.score-val{font-family:var(--mono);font-weight:700;min-width:38px;text-align:right}
|
| 139 |
.score-bar-bg{width:60px;height:4px;background:var(--s3);border-radius:99px}
|
| 140 |
.score-bar-fg{height:4px;border-radius:99px;transition:width .5s ease;background:var(--a3)}
|
| 141 |
-
.weight-tag{font-size:9px;color:var(--s3);background:var(--border);
|
| 142 |
-
padding:1px 5px;border-radius:3px;font-family:var(--mono)}
|
| 143 |
|
| 144 |
/* ββ feedback ββ */
|
| 145 |
.fb-item{font-size:11px;font-family:var(--mono);padding:5px 8px;border-radius:5px;
|
|
@@ -154,7 +148,7 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 154 |
.history-item:last-child{border:none}
|
| 155 |
.h-step{color:var(--muted);min-width:40px}
|
| 156 |
.h-reward{font-weight:700;min-width:50px}
|
| 157 |
-
.h-bar{flex:1;height:4px;background:var(--s3);border-radius:99px
|
| 158 |
.h-bar-fg{height:4px;border-radius:99px;background:var(--a3);transition:width .4s}
|
| 159 |
.h-done{color:var(--a3);font-size:10px}
|
| 160 |
|
|
@@ -194,30 +188,26 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 194 |
.tli-footer{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
| 195 |
|
| 196 |
/* ββ docs panel ββ */
|
| 197 |
-
.docs-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
| 198 |
-
|
| 199 |
-
.docs-h2{font-size:16px;font-weight:700;margin-bottom:8px;color:var(--text)}
|
| 200 |
.docs-p{font-size:13px;color:var(--muted);line-height:1.7;margin-bottom:12px}
|
| 201 |
.docs-code{background:var(--s2);border:1px solid var(--border);border-radius:6px;
|
| 202 |
-
padding:14px;font-family:var(--mono);font-size:12px;line-height:1.65;
|
| 203 |
-
overflow-x:auto;margin-bottom:12px;white-space:pre}
|
| 204 |
.method{font-weight:700;font-size:11px;padding:2px 7px;border-radius:4px;font-family:var(--mono)}
|
| 205 |
.method.post{background:rgba(86,211,100,.15);color:var(--a3)}
|
| 206 |
.method.get{background:rgba(121,192,255,.15);color:var(--a2)}
|
| 207 |
-
.ep-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;
|
| 208 |
-
border-bottom:1px solid var(--border);font-size:13px}
|
| 209 |
.ep-row:last-child{border:none}
|
| 210 |
.ep-path{font-family:var(--mono);color:var(--text);font-weight:700;min-width:180px}
|
| 211 |
.ep-desc{color:var(--muted);line-height:1.5}
|
| 212 |
|
| 213 |
-
/* ββ
|
| 214 |
.weight-bar-row{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:12px}
|
| 215 |
.wbr-name{flex:0 0 140px;font-family:var(--mono);color:var(--muted)}
|
| 216 |
.wbr-bg{flex:1;height:8px;background:var(--s3);border-radius:99px}
|
| 217 |
.wbr-fg{height:8px;border-radius:99px;background:var(--accent);transition:width .8s ease;width:0%}
|
| 218 |
.wbr-val{font-family:var(--mono);font-weight:700;color:var(--accent);min-width:36px;text-align:right}
|
| 219 |
|
| 220 |
-
/* scrollbar */
|
| 221 |
::-webkit-scrollbar{width:6px;height:6px}
|
| 222 |
::-webkit-scrollbar-track{background:var(--bg)}
|
| 223 |
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
@@ -233,8 +223,8 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 233 |
</div>
|
| 234 |
<div class="hbadges">
|
| 235 |
<span class="badge bo">v2.0.0</span>
|
| 236 |
-
<span class="badge bb">
|
| 237 |
-
<span class="badge
|
| 238 |
</div>
|
| 239 |
<div class="hstatus">
|
| 240 |
<div class="dot pulse" id="status-dot"></div>
|
|
@@ -257,10 +247,9 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 257 |
<div id="panel-playground" class="panel active">
|
| 258 |
<div class="playground">
|
| 259 |
|
| 260 |
-
<!-- LEFT
|
| 261 |
<div class="left-pane">
|
| 262 |
|
| 263 |
-
<!-- Episode controls -->
|
| 264 |
<div class="card">
|
| 265 |
<div class="card-header">
|
| 266 |
<span class="card-title">Episode Control</span>
|
|
@@ -289,7 +278,6 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 289 |
</div>
|
| 290 |
</div>
|
| 291 |
|
| 292 |
-
<!-- Code editor -->
|
| 293 |
<div class="card editor-wrap">
|
| 294 |
<div class="editor-header">
|
| 295 |
<div class="editor-dots"><span></span><span></span><span></span></div>
|
|
@@ -300,15 +288,19 @@ select:focus,input:focus{border-color:var(--accent)}
|
|
| 300 |
</div>
|
| 301 |
</div>
|
| 302 |
<textarea id="code-editor" spellcheck="false"
|
| 303 |
-
placeholder="#
|
| 304 |
-
#
|
|
|
|
|
|
|
|
|
|
| 305 |
|
| 306 |
def your_function():
|
| 307 |
pass"></textarea>
|
| 308 |
<div class="editor-footer">
|
| 309 |
<span class="char-count" id="char-count">0 chars</span>
|
| 310 |
-
<div style="display:flex;gap:8px">
|
| 311 |
<span id="step-counter" style="font-family:var(--mono);font-size:11px;color:var(--muted)">Step 0/5</span>
|
|
|
|
| 312 |
<button class="btn btn-green" id="btn-submit" onclick="doStep()" disabled>
|
| 313 |
<span id="submit-spinner" style="display:none" class="spinner"></span>
|
| 314 |
βΆ Submit
|
|
@@ -318,23 +310,24 @@ def your_function():
|
|
| 318 |
</div>
|
| 319 |
</div>
|
| 320 |
|
| 321 |
-
<!-- RIGHT
|
| 322 |
<div class="right-pane">
|
| 323 |
|
| 324 |
-
<!-- Total reward -->
|
| 325 |
<div class="card">
|
| 326 |
-
<div class="card-header">
|
|
|
|
|
|
|
|
|
|
| 327 |
<div class="card-body">
|
| 328 |
<div class="reward-big">
|
| 329 |
<div class="reward-number" id="reward-number" style="color:var(--muted)">β</div>
|
| 330 |
<div class="reward-label">/ 1.000 maximum</div>
|
| 331 |
</div>
|
| 332 |
<div class="reward-bar-bg"><div class="reward-bar" id="reward-bar"></div></div>
|
| 333 |
-
<div id="summary-text" style="font-size:12px;font-family:var(--mono);color:var(--muted);text-align:center"></div>
|
| 334 |
</div>
|
| 335 |
</div>
|
| 336 |
|
| 337 |
-
<!-- Score breakdown -->
|
| 338 |
<div class="card">
|
| 339 |
<div class="card-header"><span class="card-title">Score Breakdown</span></div>
|
| 340 |
<div class="card-body" id="score-breakdown">
|
|
@@ -342,7 +335,6 @@ def your_function():
|
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
|
| 345 |
-
<!-- Feedback -->
|
| 346 |
<div class="card">
|
| 347 |
<div class="card-header"><span class="card-title">Feedback</span></div>
|
| 348 |
<div class="card-body" id="feedback-area">
|
|
@@ -350,9 +342,11 @@ def your_function():
|
|
| 350 |
</div>
|
| 351 |
</div>
|
| 352 |
|
| 353 |
-
<!-- Step history -->
|
| 354 |
<div class="card">
|
| 355 |
-
<div class="card-header">
|
|
|
|
|
|
|
|
|
|
| 356 |
<div id="history-area">
|
| 357 |
<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>
|
| 358 |
</div>
|
|
@@ -368,36 +362,36 @@ def your_function():
|
|
| 368 |
<div id="panel-overview" class="panel">
|
| 369 |
<div class="section-label">Environment Stats</div>
|
| 370 |
<div class="grid-2">
|
| 371 |
-
<div class="stat-card"><div class="stat-val">9</div><div class="stat-label">Security Tasks
|
| 372 |
<div class="stat-card"><div class="stat-val">7</div><div class="stat-label">Reward Dimensions</div></div>
|
| 373 |
<div class="stat-card"><div class="stat-val">12+</div><div class="stat-label">CWE IDs Covered</div></div>
|
| 374 |
-
<div class="stat-card"><div class="stat-val">
|
| 375 |
</div>
|
| 376 |
|
| 377 |
<div class="section-label" style="margin-top:24px">Reward Weights</div>
|
| 378 |
<div class="card"><div class="card-body" id="weight-chart"></div></div>
|
| 379 |
|
| 380 |
-
<div class="section-label" style="margin-top:24px">
|
| 381 |
<div class="grid-2">
|
| 382 |
<div class="stat-card" style="gap:10px">
|
| 383 |
<div style="font-size:22px">βοΈ</div>
|
| 384 |
<div style="font-weight:700">Dynamic Attack Grading</div>
|
| 385 |
-
<div class="stat-label">
|
| 386 |
</div>
|
| 387 |
<div class="stat-card" style="gap:10px">
|
| 388 |
<div style="font-size:22px">π§ </div>
|
| 389 |
<div style="font-weight:700">CodeGraph Memory</div>
|
| 390 |
-
<div class="stat-label">The agent's codebase context
|
| 391 |
</div>
|
| 392 |
<div class="stat-card" style="gap:10px">
|
| 393 |
-
<div style="font-size:22px">
|
| 394 |
-
<div style="font-weight:700">
|
| 395 |
-
<div class="stat-label">
|
| 396 |
</div>
|
| 397 |
<div class="stat-card" style="gap:10px">
|
| 398 |
<div style="font-size:22px">π</div>
|
| 399 |
<div style="font-weight:700">Dense Reward Signal</div>
|
| 400 |
-
<div class="stat-label">7
|
| 401 |
</div>
|
| 402 |
</div>
|
| 403 |
</div>
|
|
@@ -406,7 +400,7 @@ def your_function():
|
|
| 406 |
<!-- TASKS PANEL -->
|
| 407 |
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 408 |
<div id="panel-tasks" class="panel">
|
| 409 |
-
<div class="section-label">All
|
| 410 |
<div style="display:flex;gap:8px;margin-bottom:16px">
|
| 411 |
<button class="btn btn-ghost" onclick="filterTasks('all')" id="f-all" style="border-color:var(--accent);color:var(--accent)">All</button>
|
| 412 |
<button class="btn btn-ghost" onclick="filterTasks('easy')" id="f-easy">Easy</button>
|
|
@@ -424,56 +418,56 @@ def your_function():
|
|
| 424 |
<div id="panel-docs" class="panel">
|
| 425 |
<div class="docs-card">
|
| 426 |
<div class="docs-h2">Quick Start</div>
|
| 427 |
-
<div class="docs-p">
|
| 428 |
<div class="docs-code">import requests
|
| 429 |
|
| 430 |
-
|
| 431 |
|
| 432 |
# 1. Start episode
|
| 433 |
-
ep = requests.post(f"{
|
| 434 |
-
sid
|
| 435 |
print(ep["problem_statement"])
|
| 436 |
|
| 437 |
-
# 2. Submit code
|
| 438 |
-
result = requests.post(f"{
|
| 439 |
"session_id": sid,
|
| 440 |
"code": "def build_user_query(u, r):\n return ('SELECT * FROM users WHERE username=%s', (u,))",
|
| 441 |
"filename": "solution.py"
|
| 442 |
}).json()
|
| 443 |
|
| 444 |
-
print(f"reward={result['total_reward']:.3f}")
|
| 445 |
print(result["feedback"]["summary"])</div>
|
| 446 |
</div>
|
| 447 |
|
| 448 |
<div class="docs-card">
|
| 449 |
<div class="docs-h2">Endpoints</div>
|
| 450 |
<div class="ep-row">
|
| 451 |
-
<div class="ep-path"><span class="method get">GET</span>
|
| 452 |
-
<div class="ep-desc">Health check. Returns
|
| 453 |
</div>
|
| 454 |
<div class="ep-row">
|
| 455 |
-
<div class="ep-path"><span class="method post">POST</span>
|
| 456 |
-
<div class="ep-desc">Start new episode. Body: <code>{"difficulty":"medium"
|
| 457 |
</div>
|
| 458 |
<div class="ep-row">
|
| 459 |
-
<div class="ep-path"><span class="method post">POST</span>
|
| 460 |
-
<div class="ep-desc">Submit code. Body: <code>{"session_id":"...","code":"...","filename":"..."}</code>. Returns reward + feedback.</div>
|
| 461 |
</div>
|
| 462 |
<div class="ep-row">
|
| 463 |
-
<div class="ep-path"><span class="method get">GET</span>
|
| 464 |
-
<div class="ep-desc">
|
| 465 |
</div>
|
| 466 |
<div class="ep-row">
|
| 467 |
-
<div class="ep-path"><span class="method get">GET</span>
|
| 468 |
-
<div class="ep-desc">List tasks.
|
| 469 |
</div>
|
| 470 |
<div class="ep-row">
|
| 471 |
-
<div class="ep-path"><span class="method get">GET</span>
|
| 472 |
-
<div class="ep-desc">Full task detail
|
| 473 |
</div>
|
| 474 |
<div class="ep-row">
|
| 475 |
-
<div class="ep-path"><span class="method get">GET</span>
|
| 476 |
-
<div class="ep-desc">Auto-generated Swagger UI
|
| 477 |
</div>
|
| 478 |
</div>
|
| 479 |
|
|
@@ -488,39 +482,39 @@ print(result["feedback"]["summary"])</div>
|
|
| 488 |
</tr>
|
| 489 |
<tr style="border-bottom:1px solid var(--border)">
|
| 490 |
<td style="padding:6px 8px;color:var(--accent)">correctness</td>
|
| 491 |
-
<td style="padding:6px 8px">
|
| 492 |
<td style="padding:6px 8px;color:var(--muted)">Custom runner</td>
|
| 493 |
<td style="padding:6px 8px;color:var(--muted)">Test cases passed</td>
|
| 494 |
</tr>
|
| 495 |
<tr style="border-bottom:1px solid var(--border)">
|
| 496 |
<td style="padding:6px 8px;color:var(--accent)">attack_resist</td>
|
| 497 |
-
<td style="padding:6px 8px">
|
| 498 |
<td style="padding:6px 8px;color:var(--muted)">Dynamic harness</td>
|
| 499 |
<td style="padding:6px 8px;color:var(--muted)">Real attack payloads blocked</td>
|
| 500 |
</tr>
|
| 501 |
<tr style="border-bottom:1px solid var(--border)">
|
| 502 |
<td style="padding:6px 8px;color:var(--accent)">static_security</td>
|
| 503 |
-
<td style="padding:6px 8px">
|
| 504 |
<td style="padding:6px 8px;color:var(--muted)">bandit + AST</td>
|
| 505 |
<td style="padding:6px 8px;color:var(--muted)">CWE-mapped vulnerability patterns</td>
|
| 506 |
</tr>
|
| 507 |
<tr style="border-bottom:1px solid var(--border)">
|
| 508 |
<td style="padding:6px 8px;color:var(--accent)">consistency</td>
|
| 509 |
-
<td style="padding:6px 8px">
|
| 510 |
<td style="padding:6px 8px;color:var(--muted)">CodeGraph</td>
|
| 511 |
-
<td style="padding:6px 8px;color:var(--muted)">
|
| 512 |
</tr>
|
| 513 |
<tr style="border-bottom:1px solid var(--border)">
|
| 514 |
<td style="padding:6px 8px;color:var(--accent)">performance</td>
|
| 515 |
-
<td style="padding:6px 8px">
|
| 516 |
<td style="padding:6px 8px;color:var(--muted)">timeit</td>
|
| 517 |
<td style="padding:6px 8px;color:var(--muted)">Speed vs naive/optimal baselines</td>
|
| 518 |
</tr>
|
| 519 |
<tr style="border-bottom:1px solid var(--border)">
|
| 520 |
<td style="padding:6px 8px;color:var(--accent)">documentation</td>
|
| 521 |
-
<td style="padding:6px 8px">
|
| 522 |
<td style="padding:6px 8px;color:var(--muted)">AST</td>
|
| 523 |
-
<td style="padding:6px 8px;color:var(--muted)">Docstrings + type
|
| 524 |
</tr>
|
| 525 |
<tr>
|
| 526 |
<td style="padding:6px 8px;color:var(--accent)">code_structure</td>
|
|
@@ -529,27 +523,52 @@ print(result["feedback"]["summary"])</div>
|
|
| 529 |
<td style="padding:6px 8px;color:var(--muted)">No bare print/except, clean structure</td>
|
| 530 |
</tr>
|
| 531 |
</table>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 532 |
</div>
|
| 533 |
</div>
|
| 534 |
</div><!-- /main -->
|
| 535 |
|
| 536 |
<script>
|
| 537 |
-
// ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 538 |
const state = {
|
| 539 |
-
sessionId: null,
|
| 540 |
-
|
| 541 |
-
stepCount: 0,
|
| 542 |
-
done: false,
|
| 543 |
-
history: [],
|
| 544 |
-
allTasks: [],
|
| 545 |
};
|
| 546 |
|
| 547 |
const WEIGHTS = {
|
| 548 |
-
correctness:0.
|
| 549 |
-
consistency:0.
|
| 550 |
};
|
| 551 |
|
| 552 |
-
// ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 553 |
document.addEventListener('DOMContentLoaded', () => {
|
| 554 |
checkHealth();
|
| 555 |
loadTasksDropdown();
|
|
@@ -558,7 +577,6 @@ document.addEventListener('DOMContentLoaded', () => {
|
|
| 558 |
updateCharCount();
|
| 559 |
});
|
| 560 |
|
| 561 |
-
// ββ Health check βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 562 |
async function checkHealth() {
|
| 563 |
const dot = document.getElementById('status-dot');
|
| 564 |
const txt = document.getElementById('status-text');
|
|
@@ -573,7 +591,6 @@ async function checkHealth() {
|
|
| 573 |
}
|
| 574 |
}
|
| 575 |
|
| 576 |
-
// ββ Tab navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 577 |
function showPanel(id, btn) {
|
| 578 |
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
| 579 |
document.querySelectorAll('.ntab').forEach(t => t.classList.remove('active'));
|
|
@@ -582,23 +599,20 @@ function showPanel(id, btn) {
|
|
| 582 |
if (id === 'tasks' && state.allTasks.length === 0) loadTasksList();
|
| 583 |
}
|
| 584 |
|
| 585 |
-
// ββ Task dropdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 586 |
async function loadTasksDropdown() {
|
| 587 |
try {
|
| 588 |
-
const
|
| 589 |
-
const tasks = await r.json();
|
| 590 |
state.allTasks = tasks;
|
| 591 |
const sel = document.getElementById('task-select');
|
| 592 |
tasks.forEach(t => {
|
| 593 |
const opt = document.createElement('option');
|
| 594 |
opt.value = t.id;
|
| 595 |
-
opt.textContent =
|
| 596 |
sel.appendChild(opt);
|
| 597 |
});
|
| 598 |
} catch(e) {}
|
| 599 |
}
|
| 600 |
|
| 601 |
-
// ββ Reset episode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 602 |
async function doReset() {
|
| 603 |
const btn = document.getElementById('btn-reset');
|
| 604 |
const spin = document.getElementById('reset-spinner');
|
|
@@ -607,42 +621,34 @@ async function doReset() {
|
|
| 607 |
|
| 608 |
const difficulty = document.getElementById('diff-select').value;
|
| 609 |
const taskId = document.getElementById('task-select').value;
|
|
|
|
| 610 |
|
| 611 |
try {
|
| 612 |
-
const body = { difficulty };
|
| 613 |
-
if (taskId) body.task_id = taskId;
|
| 614 |
const r = await fetch('/reset', {
|
| 615 |
-
method:
|
| 616 |
-
headers: {'Content-Type':'application/json'},
|
| 617 |
body: JSON.stringify(body)
|
| 618 |
});
|
| 619 |
-
if (!r.ok) {
|
| 620 |
-
const e = await r.json();
|
| 621 |
-
showAlert(e.detail || 'Reset failed', 'error');
|
| 622 |
-
return;
|
| 623 |
-
}
|
| 624 |
const d = await r.json();
|
| 625 |
state.sessionId = d.session_id;
|
| 626 |
state.task = d;
|
| 627 |
state.stepCount = 0;
|
| 628 |
state.done = false;
|
| 629 |
state.history = [];
|
| 630 |
-
|
| 631 |
renderTask(d);
|
| 632 |
resetResultPanel();
|
| 633 |
updateStepCounter();
|
| 634 |
document.getElementById('btn-submit').disabled = false;
|
| 635 |
document.getElementById('session-badge').style.display = 'inline';
|
| 636 |
-
document.getElementById('session-badge').textContent = d.session_id.slice(0,8)
|
| 637 |
showAlert(`β Episode started: ${d.task_id}`, 'success');
|
| 638 |
} catch(e) {
|
| 639 |
-
showAlert('Network error: '
|
| 640 |
} finally {
|
| 641 |
btn.disabled = false; spin.style.display = 'none';
|
| 642 |
}
|
| 643 |
}
|
| 644 |
|
| 645 |
-
// ββ Submit step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 646 |
async function doStep() {
|
| 647 |
if (!state.sessionId) { showAlert('Reset an episode first', 'error'); return; }
|
| 648 |
const code = document.getElementById('code-editor').value.trim();
|
|
@@ -655,8 +661,7 @@ async function doStep() {
|
|
| 655 |
|
| 656 |
try {
|
| 657 |
const r = await fetch('/step', {
|
| 658 |
-
method:
|
| 659 |
-
headers: {'Content-Type':'application/json'},
|
| 660 |
body: JSON.stringify({
|
| 661 |
session_id: state.sessionId,
|
| 662 |
code,
|
|
@@ -666,75 +671,64 @@ async function doStep() {
|
|
| 666 |
if (!r.ok) {
|
| 667 |
const e = await r.json();
|
| 668 |
showAlert(e.detail || 'Step failed', 'error');
|
| 669 |
-
if (r.status === 400
|
| 670 |
-
btn.disabled = true;
|
| 671 |
-
}
|
| 672 |
return;
|
| 673 |
}
|
| 674 |
const d = await r.json();
|
| 675 |
state.stepCount = d.step_count;
|
| 676 |
state.done = d.done;
|
| 677 |
-
state.history.push({
|
| 678 |
-
|
| 679 |
renderReward(d.total_reward);
|
| 680 |
renderScores(d.scores, d.details);
|
| 681 |
renderFeedback(d.feedback);
|
| 682 |
renderHistory();
|
| 683 |
updateStepCounter();
|
| 684 |
-
|
| 685 |
if (d.done) {
|
| 686 |
btn.disabled = true;
|
| 687 |
document.getElementById('done-badge').style.display = 'inline';
|
| 688 |
-
|
| 689 |
-
? 'π Excellent!
|
| 690 |
-
|
| 691 |
-
|
| 692 |
}
|
| 693 |
} catch(e) {
|
| 694 |
-
showAlert('Network error: '
|
| 695 |
} finally {
|
| 696 |
if (!state.done) btn.disabled = false;
|
| 697 |
spin.style.display = 'none';
|
| 698 |
}
|
| 699 |
}
|
| 700 |
|
| 701 |
-
// ββ Render helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 702 |
function renderTask(d) {
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
const diffClass = d.difficulty;
|
| 708 |
-
meta.innerHTML = `<span class="diff-tag ${diffClass}">${d.difficulty}</span>`
|
| 709 |
-
+ d.cwe_targets.map(c => `<span class="cwe">${c}</span>`).join('');
|
| 710 |
-
|
| 711 |
document.getElementById('task-box').textContent = d.problem_statement;
|
| 712 |
-
document.getElementById('editor-filename').textContent =
|
| 713 |
-
state.allTasks.find(t => t.id === d.task_id)?.id?.replace('_','/')+'.py' || 'solution.py';
|
| 714 |
}
|
| 715 |
|
| 716 |
function renderReward(reward) {
|
| 717 |
const n = document.getElementById('reward-number');
|
| 718 |
const bar = document.getElementById('reward-bar');
|
| 719 |
n.textContent = reward.toFixed(3);
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
bar.style.
|
|
|
|
| 723 |
}
|
| 724 |
|
| 725 |
function renderScores(scores, details) {
|
| 726 |
-
const
|
| 727 |
-
|
| 728 |
-
const
|
| 729 |
-
const
|
| 730 |
-
const w = Math.round(WEIGHTS[k] * 100);
|
| 731 |
let extra = '';
|
| 732 |
if (details) {
|
| 733 |
-
if (k
|
| 734 |
extra = ` (${details.correctness_passed}/${details.correctness_total})`;
|
| 735 |
-
|
| 736 |
-
extra = ` (${details.attacks_blocked}/${details.attacks_total}
|
| 737 |
-
}
|
| 738 |
}
|
| 739 |
return `<div class="score-row">
|
| 740 |
<div class="score-dim">${k}${extra}</div>
|
|
@@ -743,39 +737,48 @@ function renderScores(scores, details) {
|
|
| 743 |
<div class="weight-tag">${w}%</div>
|
| 744 |
</div>`;
|
| 745 |
});
|
| 746 |
-
|
| 747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
}
|
| 749 |
|
| 750 |
function renderFeedback(feedback) {
|
| 751 |
-
const el = document.getElementById('feedback-area');
|
| 752 |
const summary = feedback.summary || '';
|
| 753 |
-
const items = Object.entries(feedback).filter(([k])
|
| 754 |
-
const good =
|
| 755 |
-
const bad =
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
const cls = good(v) ? 'good' : bad(v) ? 'bad' : 'warn';
|
| 760 |
-
return `<div class="fb-item ${cls}"><strong>${k}:</strong> ${escHtml(v)}</div>`;
|
| 761 |
-
}).join('');
|
| 762 |
-
el.innerHTML = html;
|
| 763 |
}
|
| 764 |
|
| 765 |
function renderHistory() {
|
| 766 |
-
const
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
| 779 |
}
|
| 780 |
|
| 781 |
function resetResultPanel() {
|
|
@@ -793,122 +796,85 @@ function resetResultPanel() {
|
|
| 793 |
function updateStepCounter() {
|
| 794 |
document.getElementById('step-counter').textContent = `Step ${state.stepCount}/5`;
|
| 795 |
}
|
| 796 |
-
|
| 797 |
function updateCharCount() {
|
| 798 |
-
|
| 799 |
-
document.getElementById('char-count').textContent = `${len} chars`;
|
| 800 |
}
|
| 801 |
|
| 802 |
-
// ββ Editor helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 803 |
async function loadStarter() {
|
| 804 |
if (!state.task) { showAlert('Reset an episode first', 'error'); return; }
|
| 805 |
-
const tid = state.task.task_id;
|
| 806 |
try {
|
| 807 |
-
const
|
| 808 |
-
|
| 809 |
-
if (d.starter_code) {
|
| 810 |
-
document.getElementById('code-editor').value = d.starter_code;
|
| 811 |
-
updateCharCount();
|
| 812 |
-
}
|
| 813 |
} catch(e) {}
|
| 814 |
}
|
| 815 |
-
|
| 816 |
function clearEditor() {
|
| 817 |
-
document.getElementById('code-editor').value = '';
|
| 818 |
-
updateCharCount();
|
| 819 |
}
|
| 820 |
|
| 821 |
-
// ββ Alert ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 822 |
function showAlert(msg, type='info') {
|
| 823 |
-
const
|
| 824 |
-
|
| 825 |
-
|
| 826 |
-
setTimeout(() => { el.innerHTML = ''; }, 5000);
|
| 827 |
}
|
| 828 |
-
function clearAlert() { document.getElementById('alert-area').innerHTML
|
| 829 |
|
| 830 |
-
// ββ Tasks list βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 831 |
-
let taskFilter = 'all';
|
| 832 |
async function loadTasksList() {
|
| 833 |
-
if (state.allTasks.length =
|
| 834 |
-
const r = await fetch('/tasks');
|
| 835 |
-
state.allTasks = await r.json();
|
| 836 |
-
}
|
| 837 |
filterTasks('all');
|
| 838 |
}
|
| 839 |
|
| 840 |
function filterTasks(diff) {
|
| 841 |
-
taskFilter = diff;
|
| 842 |
['all','easy','medium','hard'].forEach(d => {
|
| 843 |
-
document.getElementById('f-'+d)
|
| 844 |
-
|
| 845 |
});
|
| 846 |
document.getElementById('f-'+diff).style.borderColor = 'var(--accent)';
|
| 847 |
document.getElementById('f-'+diff).style.color = 'var(--accent)';
|
| 848 |
-
|
| 849 |
-
|
| 850 |
-
|
| 851 |
-
|
| 852 |
-
|
| 853 |
-
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
<
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
<
|
| 862 |
-
</div>
|
| 863 |
-
</div>
|
| 864 |
-
`).join('');
|
| 865 |
}
|
| 866 |
|
| 867 |
-
function tryTask(
|
| 868 |
-
showPanel('playground', document.querySelector('.ntab'));
|
| 869 |
document.querySelectorAll('.ntab')[0].click();
|
| 870 |
-
document.getElementById('task-select').value =
|
| 871 |
doReset();
|
| 872 |
}
|
| 873 |
|
| 874 |
-
// ββ Weight chart βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 875 |
function renderWeightChart() {
|
| 876 |
-
|
| 877 |
-
const entries = [
|
| 878 |
-
['correctness', 0.30], ['attack_resist', 0.20],
|
| 879 |
-
['static_security', 0.15], ['consistency', 0.15],
|
| 880 |
-
['performance', 0.10], ['documentation', 0.05], ['code_structure', 0.05]
|
| 881 |
-
];
|
| 882 |
-
el.innerHTML = entries.map(([name, w]) => `
|
| 883 |
<div class="weight-bar-row">
|
| 884 |
-
<div class="wbr-name">${
|
| 885 |
-
<div class="wbr-bg"><div class="wbr-fg" style="width:${w*100*3.
|
| 886 |
<div class="wbr-val">${Math.round(w*100)}%</div>
|
| 887 |
-
</div>
|
| 888 |
-
`).join('');
|
| 889 |
-
setTimeout(() => {
|
| 890 |
-
document.querySelectorAll('.wbr-fg').forEach(b => {
|
| 891 |
-
b.style.transition = 'width .8s ease';
|
| 892 |
-
});
|
| 893 |
-
}, 100);
|
| 894 |
}
|
| 895 |
|
| 896 |
-
|
| 897 |
-
function escHtml(s) {
|
| 898 |
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
| 899 |
}
|
| 900 |
|
| 901 |
-
// Tab key in textarea
|
| 902 |
document.addEventListener('keydown', e => {
|
| 903 |
-
if (e.target.id
|
| 904 |
e.preventDefault();
|
| 905 |
-
const s
|
| 906 |
-
e.target.value
|
| 907 |
-
e.target.selectionStart
|
| 908 |
updateCharCount();
|
| 909 |
}
|
| 910 |
-
|
| 911 |
-
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') doStep();
|
| 912 |
});
|
| 913 |
</script>
|
| 914 |
</body>
|
|
|
|
| 20 |
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans)}
|
| 21 |
body{display:flex;flex-direction:column;min-height:100vh}
|
| 22 |
|
|
|
|
| 23 |
body::before{content:'';position:fixed;inset:0;
|
| 24 |
background-image:linear-gradient(rgba(240,136,62,.025) 1px,transparent 1px),
|
| 25 |
linear-gradient(90deg,rgba(240,136,62,.025) 1px,transparent 1px);
|
|
|
|
| 48 |
padding:0 24px;gap:2px;position:sticky;top:52px;z-index:100}
|
| 49 |
.ntab{font-family:var(--mono);font-size:12px;padding:10px 16px;cursor:pointer;
|
| 50 |
border-bottom:2px solid transparent;color:var(--muted);transition:.15s;
|
| 51 |
+
background:none;border-top:none;border-left:none;border-right:none}
|
| 52 |
.ntab:hover{color:var(--text)}
|
| 53 |
.ntab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
| 54 |
|
|
|
|
| 85 |
.btn-green{background:var(--a3);color:#000}
|
| 86 |
.btn-green:hover{background:#6fe87a}
|
| 87 |
.btn-green:disabled{background:var(--s3);color:var(--muted);cursor:not-allowed}
|
|
|
|
|
|
|
| 88 |
|
| 89 |
/* ββ task display ββ */
|
| 90 |
.task-box{background:var(--s2);border:1px solid var(--border);border-radius:6px;padding:14px;
|
| 91 |
+
font-size:13px;line-height:1.7;color:var(--text);white-space:pre-wrap;max-height:200px;
|
| 92 |
overflow-y:auto;font-family:var(--mono)}
|
| 93 |
.task-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
| 94 |
.cwe{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:4px;
|
|
|
|
| 121 |
|
| 122 |
/* ββ reward display ββ */
|
| 123 |
.reward-big{text-align:center;padding:20px 14px}
|
| 124 |
+
.reward-number{font-family:var(--mono);font-size:52px;font-weight:700;line-height:1;transition:all .4s ease}
|
|
|
|
| 125 |
.reward-label{font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:4px}
|
| 126 |
.reward-bar-bg{height:6px;background:var(--s3);border-radius:99px;margin:12px 0}
|
| 127 |
.reward-bar{height:6px;border-radius:99px;background:var(--accent);transition:width .6s ease;width:0%}
|
| 128 |
|
| 129 |
/* ββ score breakdown ββ */
|
| 130 |
+
.score-row{display:flex;align-items:center;gap:8px;padding:5px 0;border-bottom:1px solid var(--border);font-size:12px}
|
|
|
|
| 131 |
.score-row:last-child{border:none}
|
| 132 |
.score-dim{flex:1;color:var(--muted);font-family:var(--mono)}
|
| 133 |
.score-val{font-family:var(--mono);font-weight:700;min-width:38px;text-align:right}
|
| 134 |
.score-bar-bg{width:60px;height:4px;background:var(--s3);border-radius:99px}
|
| 135 |
.score-bar-fg{height:4px;border-radius:99px;transition:width .5s ease;background:var(--a3)}
|
| 136 |
+
.weight-tag{font-size:9px;color:var(--s3);background:var(--border);padding:1px 5px;border-radius:3px;font-family:var(--mono)}
|
|
|
|
| 137 |
|
| 138 |
/* ββ feedback ββ */
|
| 139 |
.fb-item{font-size:11px;font-family:var(--mono);padding:5px 8px;border-radius:5px;
|
|
|
|
| 148 |
.history-item:last-child{border:none}
|
| 149 |
.h-step{color:var(--muted);min-width:40px}
|
| 150 |
.h-reward{font-weight:700;min-width:50px}
|
| 151 |
+
.h-bar{flex:1;height:4px;background:var(--s3);border-radius:99px}
|
| 152 |
.h-bar-fg{height:4px;border-radius:99px;background:var(--a3);transition:width .4s}
|
| 153 |
.h-done{color:var(--a3);font-size:10px}
|
| 154 |
|
|
|
|
| 188 |
.tli-footer{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
| 189 |
|
| 190 |
/* ββ docs panel ββ */
|
| 191 |
+
.docs-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:20px;margin-bottom:12px}
|
| 192 |
+
.docs-h2{font-size:16px;font-weight:700;margin-bottom:8px}
|
|
|
|
| 193 |
.docs-p{font-size:13px;color:var(--muted);line-height:1.7;margin-bottom:12px}
|
| 194 |
.docs-code{background:var(--s2);border:1px solid var(--border);border-radius:6px;
|
| 195 |
+
padding:14px;font-family:var(--mono);font-size:12px;line-height:1.65;overflow-x:auto;margin-bottom:12px;white-space:pre}
|
|
|
|
| 196 |
.method{font-weight:700;font-size:11px;padding:2px 7px;border-radius:4px;font-family:var(--mono)}
|
| 197 |
.method.post{background:rgba(86,211,100,.15);color:var(--a3)}
|
| 198 |
.method.get{background:rgba(121,192,255,.15);color:var(--a2)}
|
| 199 |
+
.ep-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;border-bottom:1px solid var(--border);font-size:13px}
|
|
|
|
| 200 |
.ep-row:last-child{border:none}
|
| 201 |
.ep-path{font-family:var(--mono);color:var(--text);font-weight:700;min-width:180px}
|
| 202 |
.ep-desc{color:var(--muted);line-height:1.5}
|
| 203 |
|
| 204 |
+
/* ββ weight chart ββ */
|
| 205 |
.weight-bar-row{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:12px}
|
| 206 |
.wbr-name{flex:0 0 140px;font-family:var(--mono);color:var(--muted)}
|
| 207 |
.wbr-bg{flex:1;height:8px;background:var(--s3);border-radius:99px}
|
| 208 |
.wbr-fg{height:8px;border-radius:99px;background:var(--accent);transition:width .8s ease;width:0%}
|
| 209 |
.wbr-val{font-family:var(--mono);font-weight:700;color:var(--accent);min-width:36px;text-align:right}
|
| 210 |
|
|
|
|
| 211 |
::-webkit-scrollbar{width:6px;height:6px}
|
| 212 |
::-webkit-scrollbar-track{background:var(--bg)}
|
| 213 |
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
|
|
|
| 223 |
</div>
|
| 224 |
<div class="hbadges">
|
| 225 |
<span class="badge bo">v2.0.0</span>
|
| 226 |
+
<span class="badge bb">RL Environment</span>
|
| 227 |
+
<span class="badge bg">Live</span>
|
| 228 |
</div>
|
| 229 |
<div class="hstatus">
|
| 230 |
<div class="dot pulse" id="status-dot"></div>
|
|
|
|
| 247 |
<div id="panel-playground" class="panel active">
|
| 248 |
<div class="playground">
|
| 249 |
|
| 250 |
+
<!-- LEFT -->
|
| 251 |
<div class="left-pane">
|
| 252 |
|
|
|
|
| 253 |
<div class="card">
|
| 254 |
<div class="card-header">
|
| 255 |
<span class="card-title">Episode Control</span>
|
|
|
|
| 278 |
</div>
|
| 279 |
</div>
|
| 280 |
|
|
|
|
| 281 |
<div class="card editor-wrap">
|
| 282 |
<div class="editor-header">
|
| 283 |
<div class="editor-dots"><span></span><span></span><span></span></div>
|
|
|
|
| 288 |
</div>
|
| 289 |
</div>
|
| 290 |
<textarea id="code-editor" spellcheck="false"
|
| 291 |
+
placeholder="# 1. Choose a difficulty and click Reset
|
| 292 |
+
# 2. Read the task description above
|
| 293 |
+
# 3. Click 'Load starter' to see the buggy code to fix
|
| 294 |
+
# 4. Write your secure solution here
|
| 295 |
+
# 5. Press Ctrl+Enter or click Submit
|
| 296 |
|
| 297 |
def your_function():
|
| 298 |
pass"></textarea>
|
| 299 |
<div class="editor-footer">
|
| 300 |
<span class="char-count" id="char-count">0 chars</span>
|
| 301 |
+
<div style="display:flex;gap:8px;align-items:center">
|
| 302 |
<span id="step-counter" style="font-family:var(--mono);font-size:11px;color:var(--muted)">Step 0/5</span>
|
| 303 |
+
<span style="font-family:var(--mono);font-size:10px;color:var(--s3)">Ctrl+Enter</span>
|
| 304 |
<button class="btn btn-green" id="btn-submit" onclick="doStep()" disabled>
|
| 305 |
<span id="submit-spinner" style="display:none" class="spinner"></span>
|
| 306 |
βΆ Submit
|
|
|
|
| 310 |
</div>
|
| 311 |
</div>
|
| 312 |
|
| 313 |
+
<!-- RIGHT -->
|
| 314 |
<div class="right-pane">
|
| 315 |
|
|
|
|
| 316 |
<div class="card">
|
| 317 |
+
<div class="card-header">
|
| 318 |
+
<span class="card-title">Total Reward</span>
|
| 319 |
+
<span id="done-badge" style="display:none" class="badge bg">DONE β</span>
|
| 320 |
+
</div>
|
| 321 |
<div class="card-body">
|
| 322 |
<div class="reward-big">
|
| 323 |
<div class="reward-number" id="reward-number" style="color:var(--muted)">β</div>
|
| 324 |
<div class="reward-label">/ 1.000 maximum</div>
|
| 325 |
</div>
|
| 326 |
<div class="reward-bar-bg"><div class="reward-bar" id="reward-bar"></div></div>
|
| 327 |
+
<div id="summary-text" style="font-size:12px;font-family:var(--mono);color:var(--muted);text-align:center;padding:4px 0"></div>
|
| 328 |
</div>
|
| 329 |
</div>
|
| 330 |
|
|
|
|
| 331 |
<div class="card">
|
| 332 |
<div class="card-header"><span class="card-title">Score Breakdown</span></div>
|
| 333 |
<div class="card-body" id="score-breakdown">
|
|
|
|
| 335 |
</div>
|
| 336 |
</div>
|
| 337 |
|
|
|
|
| 338 |
<div class="card">
|
| 339 |
<div class="card-header"><span class="card-title">Feedback</span></div>
|
| 340 |
<div class="card-body" id="feedback-area">
|
|
|
|
| 342 |
</div>
|
| 343 |
</div>
|
| 344 |
|
|
|
|
| 345 |
<div class="card">
|
| 346 |
+
<div class="card-header">
|
| 347 |
+
<span class="card-title">Episode History</span>
|
| 348 |
+
<span class="char-count" id="history-count">0 steps</span>
|
| 349 |
+
</div>
|
| 350 |
<div id="history-area">
|
| 351 |
<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>
|
| 352 |
</div>
|
|
|
|
| 362 |
<div id="panel-overview" class="panel">
|
| 363 |
<div class="section-label">Environment Stats</div>
|
| 364 |
<div class="grid-2">
|
| 365 |
+
<div class="stat-card"><div class="stat-val">9</div><div class="stat-label">Security Tasks across 3 difficulty levels</div></div>
|
| 366 |
<div class="stat-card"><div class="stat-val">7</div><div class="stat-label">Reward Dimensions</div></div>
|
| 367 |
<div class="stat-card"><div class="stat-val">12+</div><div class="stat-label">CWE IDs Covered</div></div>
|
| 368 |
+
<div class="stat-card"><div class="stat-val">5</div><div class="stat-label">Max Steps per Episode</div></div>
|
| 369 |
</div>
|
| 370 |
|
| 371 |
<div class="section-label" style="margin-top:24px">Reward Weights</div>
|
| 372 |
<div class="card"><div class="card-body" id="weight-chart"></div></div>
|
| 373 |
|
| 374 |
+
<div class="section-label" style="margin-top:24px">Design Principles</div>
|
| 375 |
<div class="grid-2">
|
| 376 |
<div class="stat-card" style="gap:10px">
|
| 377 |
<div style="font-size:22px">βοΈ</div>
|
| 378 |
<div style="font-weight:700">Dynamic Attack Grading</div>
|
| 379 |
+
<div class="stat-label">Real SQL injection, path traversal, JWT bypass, and XSS payloads are fired at submitted code each episode. Payloads are seeded-random so agents cannot memorise them.</div>
|
| 380 |
</div>
|
| 381 |
<div class="stat-card" style="gap:10px">
|
| 382 |
<div style="font-size:22px">π§ </div>
|
| 383 |
<div style="font-weight:700">CodeGraph Memory</div>
|
| 384 |
+
<div class="stat-label">The agent's codebase context accumulates across steps. Naming conventions, error handling patterns, and type hint usage are tracked and enforced across submissions.</div>
|
| 385 |
</div>
|
| 386 |
<div class="stat-card" style="gap:10px">
|
| 387 |
+
<div style="font-size:22px">π</div>
|
| 388 |
+
<div style="font-weight:700">Security Gate</div>
|
| 389 |
+
<div class="stat-label">An episode cannot be marked done unless attack resistance β₯ 75% AND static security β₯ 70%. Functional code that is insecure will never pass.</div>
|
| 390 |
</div>
|
| 391 |
<div class="stat-card" style="gap:10px">
|
| 392 |
<div style="font-size:22px">π</div>
|
| 393 |
<div style="font-weight:700">Dense Reward Signal</div>
|
| 394 |
+
<div class="stat-label">7 dimensions give partial credit at every step. Agents receive granular feedback on what to improve rather than a binary pass/fail signal.</div>
|
| 395 |
</div>
|
| 396 |
</div>
|
| 397 |
</div>
|
|
|
|
| 400 |
<!-- TASKS PANEL -->
|
| 401 |
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 402 |
<div id="panel-tasks" class="panel">
|
| 403 |
+
<div class="section-label">All Tasks</div>
|
| 404 |
<div style="display:flex;gap:8px;margin-bottom:16px">
|
| 405 |
<button class="btn btn-ghost" onclick="filterTasks('all')" id="f-all" style="border-color:var(--accent);color:var(--accent)">All</button>
|
| 406 |
<button class="btn btn-ghost" onclick="filterTasks('easy')" id="f-easy">Easy</button>
|
|
|
|
| 418 |
<div id="panel-docs" class="panel">
|
| 419 |
<div class="docs-card">
|
| 420 |
<div class="docs-h2">Quick Start</div>
|
| 421 |
+
<div class="docs-p">Use the Playground tab for interactive testing, or call the API directly from any HTTP client.</div>
|
| 422 |
<div class="docs-code">import requests
|
| 423 |
|
| 424 |
+
BASE = "http://localhost:7860" # replace with your deployed URL
|
| 425 |
|
| 426 |
# 1. Start episode
|
| 427 |
+
ep = requests.post(f"{BASE}/reset", json={"difficulty": "medium"}).json()
|
| 428 |
+
sid = ep["session_id"]
|
| 429 |
print(ep["problem_statement"])
|
| 430 |
|
| 431 |
+
# 2. Submit code β graded across 7 dimensions
|
| 432 |
+
result = requests.post(f"{BASE}/step", json={
|
| 433 |
"session_id": sid,
|
| 434 |
"code": "def build_user_query(u, r):\n return ('SELECT * FROM users WHERE username=%s', (u,))",
|
| 435 |
"filename": "solution.py"
|
| 436 |
}).json()
|
| 437 |
|
| 438 |
+
print(f"reward = {result['total_reward']:.3f}")
|
| 439 |
print(result["feedback"]["summary"])</div>
|
| 440 |
</div>
|
| 441 |
|
| 442 |
<div class="docs-card">
|
| 443 |
<div class="docs-h2">Endpoints</div>
|
| 444 |
<div class="ep-row">
|
| 445 |
+
<div class="ep-path"><span class="method get">GET</span> /health</div>
|
| 446 |
+
<div class="ep-desc">Health check. Returns status, version, and tasks_loaded count.</div>
|
| 447 |
</div>
|
| 448 |
<div class="ep-row">
|
| 449 |
+
<div class="ep-path"><span class="method post">POST</span> /reset</div>
|
| 450 |
+
<div class="ep-desc">Start new episode. Body: <code>{"difficulty":"medium"}</code> or <code>{"task_id":"..."}</code>. Returns task, starter code, and initial CodeGraph.</div>
|
| 451 |
</div>
|
| 452 |
<div class="ep-row">
|
| 453 |
+
<div class="ep-path"><span class="method post">POST</span> /step</div>
|
| 454 |
+
<div class="ep-desc">Submit code. Body: <code>{"session_id":"...","code":"...","filename":"..."}</code>. Returns reward + per-dimension scores + feedback.</div>
|
| 455 |
</div>
|
| 456 |
<div class="ep-row">
|
| 457 |
+
<div class="ep-path"><span class="method get">GET</span> /state</div>
|
| 458 |
+
<div class="ep-desc">Current episode state. Query param: <code>session_id</code></div>
|
| 459 |
</div>
|
| 460 |
<div class="ep-row">
|
| 461 |
+
<div class="ep-path"><span class="method get">GET</span> /tasks</div>
|
| 462 |
+
<div class="ep-desc">List all tasks. Optional filter: <code>?difficulty=easy</code></div>
|
| 463 |
</div>
|
| 464 |
<div class="ep-row">
|
| 465 |
+
<div class="ep-path"><span class="method get">GET</span> /tasks/{id}</div>
|
| 466 |
+
<div class="ep-desc">Full task detail β problem statement, starter code, security checks.</div>
|
| 467 |
</div>
|
| 468 |
<div class="ep-row">
|
| 469 |
+
<div class="ep-path"><span class="method get">GET</span> /docs</div>
|
| 470 |
+
<div class="ep-desc">Auto-generated Swagger UI with full schema documentation.</div>
|
| 471 |
</div>
|
| 472 |
</div>
|
| 473 |
|
|
|
|
| 482 |
</tr>
|
| 483 |
<tr style="border-bottom:1px solid var(--border)">
|
| 484 |
<td style="padding:6px 8px;color:var(--accent)">correctness</td>
|
| 485 |
+
<td style="padding:6px 8px">25%</td>
|
| 486 |
<td style="padding:6px 8px;color:var(--muted)">Custom runner</td>
|
| 487 |
<td style="padding:6px 8px;color:var(--muted)">Test cases passed</td>
|
| 488 |
</tr>
|
| 489 |
<tr style="border-bottom:1px solid var(--border)">
|
| 490 |
<td style="padding:6px 8px;color:var(--accent)">attack_resist</td>
|
| 491 |
+
<td style="padding:6px 8px">25%</td>
|
| 492 |
<td style="padding:6px 8px;color:var(--muted)">Dynamic harness</td>
|
| 493 |
<td style="padding:6px 8px;color:var(--muted)">Real attack payloads blocked</td>
|
| 494 |
</tr>
|
| 495 |
<tr style="border-bottom:1px solid var(--border)">
|
| 496 |
<td style="padding:6px 8px;color:var(--accent)">static_security</td>
|
| 497 |
+
<td style="padding:6px 8px">20%</td>
|
| 498 |
<td style="padding:6px 8px;color:var(--muted)">bandit + AST</td>
|
| 499 |
<td style="padding:6px 8px;color:var(--muted)">CWE-mapped vulnerability patterns</td>
|
| 500 |
</tr>
|
| 501 |
<tr style="border-bottom:1px solid var(--border)">
|
| 502 |
<td style="padding:6px 8px;color:var(--accent)">consistency</td>
|
| 503 |
+
<td style="padding:6px 8px">10%</td>
|
| 504 |
<td style="padding:6px 8px;color:var(--muted)">CodeGraph</td>
|
| 505 |
+
<td style="padding:6px 8px;color:var(--muted)">Convention adherence across steps</td>
|
| 506 |
</tr>
|
| 507 |
<tr style="border-bottom:1px solid var(--border)">
|
| 508 |
<td style="padding:6px 8px;color:var(--accent)">performance</td>
|
| 509 |
+
<td style="padding:6px 8px">8%</td>
|
| 510 |
<td style="padding:6px 8px;color:var(--muted)">timeit</td>
|
| 511 |
<td style="padding:6px 8px;color:var(--muted)">Speed vs naive/optimal baselines</td>
|
| 512 |
</tr>
|
| 513 |
<tr style="border-bottom:1px solid var(--border)">
|
| 514 |
<td style="padding:6px 8px;color:var(--accent)">documentation</td>
|
| 515 |
+
<td style="padding:6px 8px">7%</td>
|
| 516 |
<td style="padding:6px 8px;color:var(--muted)">AST</td>
|
| 517 |
+
<td style="padding:6px 8px;color:var(--muted)">Docstrings + type hint coverage</td>
|
| 518 |
</tr>
|
| 519 |
<tr>
|
| 520 |
<td style="padding:6px 8px;color:var(--accent)">code_structure</td>
|
|
|
|
| 523 |
<td style="padding:6px 8px;color:var(--muted)">No bare print/except, clean structure</td>
|
| 524 |
</tr>
|
| 525 |
</table>
|
| 526 |
+
<div style="margin-top:12px;padding:10px;background:var(--s2);border-radius:6px;font-size:11px;font-family:var(--mono);color:var(--warn)">
|
| 527 |
+
β Security gate: episode cannot complete unless attack_resist β₯ 0.75 AND static_security β₯ 0.70 AND correctness β₯ 0.80
|
| 528 |
+
</div>
|
| 529 |
+
</div>
|
| 530 |
+
|
| 531 |
+
<div class="docs-card">
|
| 532 |
+
<div class="docs-h2">Step Response Example</div>
|
| 533 |
+
<div class="docs-code">{
|
| 534 |
+
"total_reward": 0.847,
|
| 535 |
+
"scores": {
|
| 536 |
+
"correctness": 1.0,
|
| 537 |
+
"attack_resist": 0.875,
|
| 538 |
+
"static_security": 0.9,
|
| 539 |
+
"consistency": 0.75,
|
| 540 |
+
"performance": 0.6,
|
| 541 |
+
"documentation": 0.75,
|
| 542 |
+
"code_structure": 0.8
|
| 543 |
+
},
|
| 544 |
+
"feedback": {
|
| 545 |
+
"summary": "π‘ Good (0.847) β improve: consistency (0.75)",
|
| 546 |
+
"attack_resist": "Good β SQL injection attacks blocked (87%)",
|
| 547 |
+
"security_gate": "PASSED"
|
| 548 |
+
},
|
| 549 |
+
"details": {
|
| 550 |
+
"correctness": {"passed": 5, "total": 5},
|
| 551 |
+
"attacks": {"blocked": 7, "total": 8, "type": "injection"},
|
| 552 |
+
"security_gate_passed": true
|
| 553 |
+
},
|
| 554 |
+
"done": false,
|
| 555 |
+
"step_count": 1
|
| 556 |
+
}</div>
|
| 557 |
</div>
|
| 558 |
</div>
|
| 559 |
</div><!-- /main -->
|
| 560 |
|
| 561 |
<script>
|
|
|
|
| 562 |
const state = {
|
| 563 |
+
sessionId: null, task: null, stepCount: 0,
|
| 564 |
+
done: false, history: [], allTasks: [],
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
};
|
| 566 |
|
| 567 |
const WEIGHTS = {
|
| 568 |
+
correctness:0.25, attack_resist:0.25, static_security:0.20,
|
| 569 |
+
consistency:0.10, performance:0.08, documentation:0.07, code_structure:0.05
|
| 570 |
};
|
| 571 |
|
|
|
|
| 572 |
document.addEventListener('DOMContentLoaded', () => {
|
| 573 |
checkHealth();
|
| 574 |
loadTasksDropdown();
|
|
|
|
| 577 |
updateCharCount();
|
| 578 |
});
|
| 579 |
|
|
|
|
| 580 |
async function checkHealth() {
|
| 581 |
const dot = document.getElementById('status-dot');
|
| 582 |
const txt = document.getElementById('status-text');
|
|
|
|
| 591 |
}
|
| 592 |
}
|
| 593 |
|
|
|
|
| 594 |
function showPanel(id, btn) {
|
| 595 |
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
| 596 |
document.querySelectorAll('.ntab').forEach(t => t.classList.remove('active'));
|
|
|
|
| 599 |
if (id === 'tasks' && state.allTasks.length === 0) loadTasksList();
|
| 600 |
}
|
| 601 |
|
|
|
|
| 602 |
async function loadTasksDropdown() {
|
| 603 |
try {
|
| 604 |
+
const tasks = await (await fetch('/tasks')).json();
|
|
|
|
| 605 |
state.allTasks = tasks;
|
| 606 |
const sel = document.getElementById('task-select');
|
| 607 |
tasks.forEach(t => {
|
| 608 |
const opt = document.createElement('option');
|
| 609 |
opt.value = t.id;
|
| 610 |
+
opt.textContent = t.id.replace(/_/g,' ');
|
| 611 |
sel.appendChild(opt);
|
| 612 |
});
|
| 613 |
} catch(e) {}
|
| 614 |
}
|
| 615 |
|
|
|
|
| 616 |
async function doReset() {
|
| 617 |
const btn = document.getElementById('btn-reset');
|
| 618 |
const spin = document.getElementById('reset-spinner');
|
|
|
|
| 621 |
|
| 622 |
const difficulty = document.getElementById('diff-select').value;
|
| 623 |
const taskId = document.getElementById('task-select').value;
|
| 624 |
+
const body = taskId ? {task_id: taskId} : {difficulty};
|
| 625 |
|
| 626 |
try {
|
|
|
|
|
|
|
| 627 |
const r = await fetch('/reset', {
|
| 628 |
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
|
|
| 629 |
body: JSON.stringify(body)
|
| 630 |
});
|
| 631 |
+
if (!r.ok) { showAlert((await r.json()).detail || 'Reset failed', 'error'); return; }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 632 |
const d = await r.json();
|
| 633 |
state.sessionId = d.session_id;
|
| 634 |
state.task = d;
|
| 635 |
state.stepCount = 0;
|
| 636 |
state.done = false;
|
| 637 |
state.history = [];
|
|
|
|
| 638 |
renderTask(d);
|
| 639 |
resetResultPanel();
|
| 640 |
updateStepCounter();
|
| 641 |
document.getElementById('btn-submit').disabled = false;
|
| 642 |
document.getElementById('session-badge').style.display = 'inline';
|
| 643 |
+
document.getElementById('session-badge').textContent = d.session_id.slice(0,8)+'β¦';
|
| 644 |
showAlert(`β Episode started: ${d.task_id}`, 'success');
|
| 645 |
} catch(e) {
|
| 646 |
+
showAlert('Network error: '+e.message, 'error');
|
| 647 |
} finally {
|
| 648 |
btn.disabled = false; spin.style.display = 'none';
|
| 649 |
}
|
| 650 |
}
|
| 651 |
|
|
|
|
| 652 |
async function doStep() {
|
| 653 |
if (!state.sessionId) { showAlert('Reset an episode first', 'error'); return; }
|
| 654 |
const code = document.getElementById('code-editor').value.trim();
|
|
|
|
| 661 |
|
| 662 |
try {
|
| 663 |
const r = await fetch('/step', {
|
| 664 |
+
method:'POST', headers:{'Content-Type':'application/json'},
|
|
|
|
| 665 |
body: JSON.stringify({
|
| 666 |
session_id: state.sessionId,
|
| 667 |
code,
|
|
|
|
| 671 |
if (!r.ok) {
|
| 672 |
const e = await r.json();
|
| 673 |
showAlert(e.detail || 'Step failed', 'error');
|
| 674 |
+
if (r.status === 400) btn.disabled = true;
|
|
|
|
|
|
|
| 675 |
return;
|
| 676 |
}
|
| 677 |
const d = await r.json();
|
| 678 |
state.stepCount = d.step_count;
|
| 679 |
state.done = d.done;
|
| 680 |
+
state.history.push({step: d.step_count, reward: d.total_reward, done: d.done});
|
|
|
|
| 681 |
renderReward(d.total_reward);
|
| 682 |
renderScores(d.scores, d.details);
|
| 683 |
renderFeedback(d.feedback);
|
| 684 |
renderHistory();
|
| 685 |
updateStepCounter();
|
|
|
|
| 686 |
if (d.done) {
|
| 687 |
btn.disabled = true;
|
| 688 |
document.getElementById('done-badge').style.display = 'inline';
|
| 689 |
+
showAlert(
|
| 690 |
+
d.total_reward >= 0.92 ? 'π Excellent! Security gate passed!' : `Episode complete after ${d.step_count} steps.`,
|
| 691 |
+
d.total_reward >= 0.92 ? 'success' : 'info'
|
| 692 |
+
);
|
| 693 |
}
|
| 694 |
} catch(e) {
|
| 695 |
+
showAlert('Network error: '+e.message, 'error');
|
| 696 |
} finally {
|
| 697 |
if (!state.done) btn.disabled = false;
|
| 698 |
spin.style.display = 'none';
|
| 699 |
}
|
| 700 |
}
|
| 701 |
|
|
|
|
| 702 |
function renderTask(d) {
|
| 703 |
+
document.getElementById('task-area').style.display = 'block';
|
| 704 |
+
document.getElementById('task-meta').innerHTML =
|
| 705 |
+
`<span class="diff-tag ${d.difficulty}">${d.difficulty}</span>` +
|
| 706 |
+
d.cwe_targets.map(c=>`<span class="cwe">${c}</span>`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 707 |
document.getElementById('task-box').textContent = d.problem_statement;
|
| 708 |
+
document.getElementById('editor-filename').textContent = d.task_id+'.py';
|
|
|
|
| 709 |
}
|
| 710 |
|
| 711 |
function renderReward(reward) {
|
| 712 |
const n = document.getElementById('reward-number');
|
| 713 |
const bar = document.getElementById('reward-bar');
|
| 714 |
n.textContent = reward.toFixed(3);
|
| 715 |
+
const color = reward >= 0.92 ? 'var(--a3)' : reward >= 0.65 ? 'var(--accent)' : 'var(--danger)';
|
| 716 |
+
n.style.color = color;
|
| 717 |
+
bar.style.width = (reward*100)+'%';
|
| 718 |
+
bar.style.background = color;
|
| 719 |
}
|
| 720 |
|
| 721 |
function renderScores(scores, details) {
|
| 722 |
+
const rows = Object.entries(scores).map(([k,v]) => {
|
| 723 |
+
const pct = Math.round(v*100);
|
| 724 |
+
const color = v >= 0.75 ? 'var(--a3)' : v >= 0.5 ? 'var(--accent)' : 'var(--danger)';
|
| 725 |
+
const w = Math.round((WEIGHTS[k]||0)*100);
|
|
|
|
| 726 |
let extra = '';
|
| 727 |
if (details) {
|
| 728 |
+
if (k==='correctness' && details.correctness_total)
|
| 729 |
extra = ` (${details.correctness_passed}/${details.correctness_total})`;
|
| 730 |
+
else if (k==='attack_resist' && details.attacks_total)
|
| 731 |
+
extra = ` (${details.attacks_blocked}/${details.attacks_total})`;
|
|
|
|
| 732 |
}
|
| 733 |
return `<div class="score-row">
|
| 734 |
<div class="score-dim">${k}${extra}</div>
|
|
|
|
| 737 |
<div class="weight-tag">${w}%</div>
|
| 738 |
</div>`;
|
| 739 |
});
|
| 740 |
+
document.getElementById('score-breakdown').innerHTML = rows.join('');
|
| 741 |
+
// Security gate status
|
| 742 |
+
if (details) {
|
| 743 |
+
const gateEl = document.createElement('div');
|
| 744 |
+
gateEl.style.cssText = 'margin-top:8px;font-family:var(--mono);font-size:10px;padding:6px 8px;border-radius:4px;';
|
| 745 |
+
if (details.security_gate_passed) {
|
| 746 |
+
gateEl.style.background = 'rgba(86,211,100,.1)';
|
| 747 |
+
gateEl.style.color = 'var(--a3)';
|
| 748 |
+
gateEl.textContent = 'π Security gate: PASSED';
|
| 749 |
+
} else {
|
| 750 |
+
gateEl.style.background = 'rgba(255,123,114,.1)';
|
| 751 |
+
gateEl.style.color = 'var(--danger)';
|
| 752 |
+
gateEl.textContent = 'π Security gate: NOT MET β need attackβ₯0.75, staticβ₯0.70, correctnessβ₯0.80';
|
| 753 |
+
}
|
| 754 |
+
document.getElementById('score-breakdown').appendChild(gateEl);
|
| 755 |
+
}
|
| 756 |
}
|
| 757 |
|
| 758 |
function renderFeedback(feedback) {
|
|
|
|
| 759 |
const summary = feedback.summary || '';
|
| 760 |
+
const items = Object.entries(feedback).filter(([k])=>k!=='summary'&&k!=='security_gate');
|
| 761 |
+
const good = v => v.startsWith('Excellent')||v.startsWith('Clean')||v.startsWith('Well')||v.startsWith('Good conv');
|
| 762 |
+
const bad = v => v.includes('CRITICAL')||v.includes('Vulnerable')||v.includes('HIGH')||v.includes('Poor');
|
| 763 |
+
document.getElementById('feedback-area').innerHTML =
|
| 764 |
+
`<div class="fb-item ${summary.includes('β
')?'good':summary.includes('π΄')||summary.includes('π')?'bad':'warn'}">${esc(summary)}</div>` +
|
| 765 |
+
items.map(([k,v])=>`<div class="fb-item ${good(v)?'good':bad(v)?'bad':'warn'}"><strong>${k}:</strong> ${esc(v)}</div>`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
| 766 |
}
|
| 767 |
|
| 768 |
function renderHistory() {
|
| 769 |
+
const n = state.history.length;
|
| 770 |
+
document.getElementById('history-count').textContent = `${n} step${n!==1?'s':''}`;
|
| 771 |
+
document.getElementById('history-area').innerHTML = !n
|
| 772 |
+
? '<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>'
|
| 773 |
+
: state.history.map(h => {
|
| 774 |
+
const color = h.reward >= 0.92 ? 'var(--a3)' : h.reward >= 0.65 ? 'var(--accent)' : 'var(--danger)';
|
| 775 |
+
return `<div class="history-item">
|
| 776 |
+
<span class="h-step">Step ${h.step}</span>
|
| 777 |
+
<span class="h-reward" style="color:${color}">${h.reward.toFixed(3)}</span>
|
| 778 |
+
<div class="h-bar"><div class="h-bar-fg" style="width:${h.reward*100}%;background:${color}"></div></div>
|
| 779 |
+
${h.done ? '<span class="h-done">done</span>' : ''}
|
| 780 |
+
</div>`;
|
| 781 |
+
}).join('');
|
| 782 |
}
|
| 783 |
|
| 784 |
function resetResultPanel() {
|
|
|
|
| 796 |
function updateStepCounter() {
|
| 797 |
document.getElementById('step-counter').textContent = `Step ${state.stepCount}/5`;
|
| 798 |
}
|
|
|
|
| 799 |
function updateCharCount() {
|
| 800 |
+
document.getElementById('char-count').textContent = document.getElementById('code-editor').value.length+' chars';
|
|
|
|
| 801 |
}
|
| 802 |
|
|
|
|
| 803 |
async function loadStarter() {
|
| 804 |
if (!state.task) { showAlert('Reset an episode first', 'error'); return; }
|
|
|
|
| 805 |
try {
|
| 806 |
+
const d = await (await fetch(`/tasks/${state.task.task_id}`)).json();
|
| 807 |
+
if (d.starter_code) { document.getElementById('code-editor').value = d.starter_code; updateCharCount(); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 808 |
} catch(e) {}
|
| 809 |
}
|
|
|
|
| 810 |
function clearEditor() {
|
| 811 |
+
document.getElementById('code-editor').value = ''; updateCharCount();
|
|
|
|
| 812 |
}
|
| 813 |
|
|
|
|
| 814 |
function showAlert(msg, type='info') {
|
| 815 |
+
const cls = type==='error'?'alert-error':type==='success'?'alert-success':'alert-info';
|
| 816 |
+
document.getElementById('alert-area').innerHTML = `<div class="alert ${cls}">${esc(msg)}</div>`;
|
| 817 |
+
setTimeout(()=>{ document.getElementById('alert-area').innerHTML=''; }, 5000);
|
|
|
|
| 818 |
}
|
| 819 |
+
function clearAlert() { document.getElementById('alert-area').innerHTML=''; }
|
| 820 |
|
|
|
|
|
|
|
| 821 |
async function loadTasksList() {
|
| 822 |
+
if (!state.allTasks.length) state.allTasks = await (await fetch('/tasks')).json();
|
|
|
|
|
|
|
|
|
|
| 823 |
filterTasks('all');
|
| 824 |
}
|
| 825 |
|
| 826 |
function filterTasks(diff) {
|
|
|
|
| 827 |
['all','easy','medium','hard'].forEach(d => {
|
| 828 |
+
const el = document.getElementById('f-'+d);
|
| 829 |
+
el.style.borderColor = ''; el.style.color = '';
|
| 830 |
});
|
| 831 |
document.getElementById('f-'+diff).style.borderColor = 'var(--accent)';
|
| 832 |
document.getElementById('f-'+diff).style.color = 'var(--accent)';
|
| 833 |
+
const tasks = diff==='all' ? state.allTasks : state.allTasks.filter(t=>t.difficulty===diff);
|
| 834 |
+
document.getElementById('task-list-container').innerHTML = !tasks.length
|
| 835 |
+
? '<div class="empty"><div class="empty-text">No tasks found</div></div>'
|
| 836 |
+
: tasks.map(t=>`
|
| 837 |
+
<div class="task-list-item" onclick="tryTask('${t.id}')">
|
| 838 |
+
<div class="tli-header">
|
| 839 |
+
<div class="tli-name">${t.id.replace(/_/g,' ')}</div>
|
| 840 |
+
<span class="diff-tag ${t.difficulty}">${t.difficulty}</span>
|
| 841 |
+
</div>
|
| 842 |
+
<div class="tli-desc">${esc((t.description||'').slice(0,120))}${(t.description||'').length>120?'β¦':''}</div>
|
| 843 |
+
<div class="tli-footer">
|
| 844 |
+
${t.cwe_targets.map(c=>`<span class="cwe">${c}</span>`).join('')}
|
| 845 |
+
<span class="badge bo" style="font-size:9px;margin-left:auto">Try β</span>
|
| 846 |
+
</div>
|
| 847 |
+
</div>`).join('');
|
|
|
|
|
|
|
| 848 |
}
|
| 849 |
|
| 850 |
+
function tryTask(id) {
|
|
|
|
| 851 |
document.querySelectorAll('.ntab')[0].click();
|
| 852 |
+
document.getElementById('task-select').value = id;
|
| 853 |
doReset();
|
| 854 |
}
|
| 855 |
|
|
|
|
| 856 |
function renderWeightChart() {
|
| 857 |
+
document.getElementById('weight-chart').innerHTML = Object.entries(WEIGHTS).map(([n,w])=>`
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 858 |
<div class="weight-bar-row">
|
| 859 |
+
<div class="wbr-name">${n}</div>
|
| 860 |
+
<div class="wbr-bg"><div class="wbr-fg" style="width:${w*100*3.3}%"></div></div>
|
| 861 |
<div class="wbr-val">${Math.round(w*100)}%</div>
|
| 862 |
+
</div>`).join('');
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 863 |
}
|
| 864 |
|
| 865 |
+
function esc(s) {
|
|
|
|
| 866 |
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
| 867 |
}
|
| 868 |
|
|
|
|
| 869 |
document.addEventListener('keydown', e => {
|
| 870 |
+
if (e.target.id==='code-editor' && e.key==='Tab') {
|
| 871 |
e.preventDefault();
|
| 872 |
+
const s=e.target.selectionStart, en=e.target.selectionEnd;
|
| 873 |
+
e.target.value=e.target.value.substring(0,s)+' '+e.target.value.substring(en);
|
| 874 |
+
e.target.selectionStart=e.target.selectionEnd=s+4;
|
| 875 |
updateCharCount();
|
| 876 |
}
|
| 877 |
+
if ((e.ctrlKey||e.metaKey) && e.key==='Enter') doStep();
|
|
|
|
| 878 |
});
|
| 879 |
</script>
|
| 880 |
</body>
|
dashboard.html
ADDED
|
@@ -0,0 +1,913 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
| 6 |
+
<title>SecureCodeEnv β RL Playground</title>
|
| 7 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
| 8 |
+
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:ital,wght@0,400;0,700;1,400&family=Syne:wght@500;700;800&display=swap" rel="stylesheet">
|
| 9 |
+
<style>
|
| 10 |
+
:root{
|
| 11 |
+
--bg:#07090d;--surface:#0d1117;--s2:#161b22;--s3:#21262d;
|
| 12 |
+
--border:#30363d;--accent:#f0883e;--a2:#79c0ff;--a3:#56d364;
|
| 13 |
+
--danger:#ff7b72;--warn:#e3b341;--text:#e6edf3;--muted:#8b949e;
|
| 14 |
+
--mono:'JetBrains Mono',monospace;--sans:'Syne',sans-serif;
|
| 15 |
+
--radius:8px;
|
| 16 |
+
}
|
| 17 |
+
*{box-sizing:border-box;margin:0;padding:0}
|
| 18 |
+
html,body{height:100%;background:var(--bg);color:var(--text);font-family:var(--sans)}
|
| 19 |
+
body{display:flex;flex-direction:column;min-height:100vh}
|
| 20 |
+
|
| 21 |
+
/* grid bg */
|
| 22 |
+
body::before{content:'';position:fixed;inset:0;
|
| 23 |
+
background-image:linear-gradient(rgba(240,136,62,.025) 1px,transparent 1px),
|
| 24 |
+
linear-gradient(90deg,rgba(240,136,62,.025) 1px,transparent 1px);
|
| 25 |
+
background-size:48px 48px;pointer-events:none;z-index:0}
|
| 26 |
+
|
| 27 |
+
/* ββ header ββ */
|
| 28 |
+
header{position:sticky;top:0;z-index:200;background:rgba(7,9,13,.88);
|
| 29 |
+
backdrop-filter:blur(12px);border-bottom:1px solid var(--border);
|
| 30 |
+
padding:0 24px;height:52px;display:flex;align-items:center;justify-content:space-between;gap:16px}
|
| 31 |
+
.hlogo{display:flex;align-items:center;gap:10px;font-family:var(--mono);font-weight:700;font-size:14px;color:var(--accent)}
|
| 32 |
+
.hlogo-icon{width:26px;height:26px;background:var(--accent);border-radius:5px;display:grid;place-items:center;font-size:13px;color:#000}
|
| 33 |
+
.hbadges{display:flex;gap:6px;flex-wrap:wrap}
|
| 34 |
+
.badge{font-family:var(--mono);font-size:10px;padding:2px 8px;border-radius:99px;border:1px solid;letter-spacing:.4px}
|
| 35 |
+
.bo{color:var(--accent);border-color:rgba(240,136,62,.3);background:rgba(240,136,62,.07)}
|
| 36 |
+
.bb{color:var(--a2);border-color:rgba(121,192,255,.3);background:rgba(121,192,255,.07)}
|
| 37 |
+
.bg{color:var(--a3);border-color:rgba(86,211,100,.3);background:rgba(86,211,100,.07)}
|
| 38 |
+
.br{color:var(--danger);border-color:rgba(255,123,114,.3);background:rgba(255,123,114,.07)}
|
| 39 |
+
.hstatus{display:flex;align-items:center;gap:8px;font-size:12px;font-family:var(--mono)}
|
| 40 |
+
.dot{width:7px;height:7px;border-radius:50%;background:var(--a3);box-shadow:0 0 6px var(--a3)}
|
| 41 |
+
.dot.red{background:var(--danger);box-shadow:0 0 6px var(--danger)}
|
| 42 |
+
.dot.pulse{animation:pulse 2s ease infinite}
|
| 43 |
+
@keyframes pulse{0%,100%{opacity:1}50%{opacity:.35}}
|
| 44 |
+
|
| 45 |
+
/* ββ nav tabs ββ */
|
| 46 |
+
.nav{display:flex;border-bottom:1px solid var(--border);background:var(--surface);
|
| 47 |
+
padding:0 24px;gap:2px;position:sticky;top:52px;z-index:100}
|
| 48 |
+
.ntab{font-family:var(--mono);font-size:12px;padding:10px 16px;cursor:pointer;
|
| 49 |
+
border-bottom:2px solid transparent;color:var(--muted);transition:.15s;
|
| 50 |
+
background:none;border-top:none;border-left:none;border-right:none;color:var(--muted)}
|
| 51 |
+
.ntab:hover{color:var(--text)}
|
| 52 |
+
.ntab.active{color:var(--accent);border-bottom-color:var(--accent)}
|
| 53 |
+
|
| 54 |
+
/* ββ main layout ββ */
|
| 55 |
+
.main{position:relative;z-index:1;flex:1;padding:24px;max-width:1200px;margin:0 auto;width:100%}
|
| 56 |
+
.panel{display:none}
|
| 57 |
+
.panel.active{display:block}
|
| 58 |
+
|
| 59 |
+
/* ββ playground layout ββ */
|
| 60 |
+
.playground{display:grid;grid-template-columns:1fr 400px;gap:16px;height:calc(100vh - 160px)}
|
| 61 |
+
@media(max-width:900px){.playground{grid-template-columns:1fr;height:auto}}
|
| 62 |
+
|
| 63 |
+
/* ββ left pane ββ */
|
| 64 |
+
.left-pane{display:flex;flex-direction:column;gap:12px;min-height:0}
|
| 65 |
+
.card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);overflow:hidden}
|
| 66 |
+
.card-header{display:flex;align-items:center;justify-content:space-between;
|
| 67 |
+
padding:10px 14px;border-bottom:1px solid var(--border);background:var(--s2)}
|
| 68 |
+
.card-title{font-size:11px;font-family:var(--mono);color:var(--muted);letter-spacing:1px;text-transform:uppercase}
|
| 69 |
+
.card-body{padding:14px}
|
| 70 |
+
|
| 71 |
+
/* ββ controls ββ */
|
| 72 |
+
.controls-row{display:flex;gap:8px;flex-wrap:wrap;align-items:center}
|
| 73 |
+
select,input[type=text]{font-family:var(--mono);font-size:12px;background:var(--s2);
|
| 74 |
+
border:1px solid var(--border);color:var(--text);border-radius:5px;padding:7px 10px;
|
| 75 |
+
outline:none;transition:border-color .15s}
|
| 76 |
+
select:focus,input:focus{border-color:var(--accent)}
|
| 77 |
+
.btn{font-family:var(--mono);font-size:12px;font-weight:700;padding:7px 16px;
|
| 78 |
+
border-radius:5px;border:none;cursor:pointer;transition:all .12s;display:inline-flex;align-items:center;gap:6px}
|
| 79 |
+
.btn-primary{background:var(--accent);color:#000}
|
| 80 |
+
.btn-primary:hover{background:#ffaa5e;transform:translateY(-1px)}
|
| 81 |
+
.btn-primary:disabled{background:var(--s3);color:var(--muted);cursor:not-allowed;transform:none}
|
| 82 |
+
.btn-ghost{background:transparent;color:var(--text);border:1px solid var(--border)}
|
| 83 |
+
.btn-ghost:hover{border-color:var(--a2);color:var(--a2)}
|
| 84 |
+
.btn-green{background:var(--a3);color:#000}
|
| 85 |
+
.btn-green:hover{background:#6fe87a}
|
| 86 |
+
.btn-green:disabled{background:var(--s3);color:var(--muted);cursor:not-allowed}
|
| 87 |
+
.btn-danger{background:transparent;color:var(--danger);border:1px solid rgba(255,123,114,.3)}
|
| 88 |
+
.btn-danger:hover{background:rgba(255,123,114,.1)}
|
| 89 |
+
|
| 90 |
+
/* ββ task display ββ */
|
| 91 |
+
.task-box{background:var(--s2);border:1px solid var(--border);border-radius:6px;padding:14px;
|
| 92 |
+
font-size:13px;line-height:1.7;color:var(--text);white-space:pre-wrap;max-height:180px;
|
| 93 |
+
overflow-y:auto;font-family:var(--mono)}
|
| 94 |
+
.task-meta{display:flex;gap:8px;flex-wrap:wrap;margin-bottom:8px}
|
| 95 |
+
.cwe{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:4px;
|
| 96 |
+
background:rgba(121,192,255,.08);color:var(--a2);border:1px solid rgba(121,192,255,.2)}
|
| 97 |
+
.diff-tag{font-family:var(--mono);font-size:10px;padding:2px 7px;border-radius:4px}
|
| 98 |
+
.easy{background:rgba(86,211,100,.1);color:var(--a3)}
|
| 99 |
+
.medium{background:rgba(240,136,62,.1);color:var(--accent)}
|
| 100 |
+
.hard{background:rgba(255,123,114,.1);color:var(--danger)}
|
| 101 |
+
|
| 102 |
+
/* ββ code editor ββ */
|
| 103 |
+
.editor-wrap{flex:1;display:flex;flex-direction:column;min-height:0}
|
| 104 |
+
.editor-header{display:flex;align-items:center;justify-content:space-between;
|
| 105 |
+
padding:8px 14px;background:var(--s2);border-bottom:1px solid var(--border)}
|
| 106 |
+
.editor-dots{display:flex;gap:5px}
|
| 107 |
+
.editor-dots span{width:9px;height:9px;border-radius:50%}
|
| 108 |
+
.editor-dots span:nth-child(1){background:#ff5f57}
|
| 109 |
+
.editor-dots span:nth-child(2){background:#febc2e}
|
| 110 |
+
.editor-dots span:nth-child(3){background:#28c840}
|
| 111 |
+
#code-editor{flex:1;width:100%;background:var(--s2);border:none;color:var(--text);
|
| 112 |
+
font-family:var(--mono);font-size:12px;line-height:1.65;padding:16px;
|
| 113 |
+
resize:none;outline:none;tab-size:4;min-height:280px}
|
| 114 |
+
#code-editor::placeholder{color:var(--muted)}
|
| 115 |
+
.editor-footer{padding:8px 14px;background:var(--s2);border-top:1px solid var(--border);
|
| 116 |
+
display:flex;justify-content:space-between;align-items:center;gap:8px}
|
| 117 |
+
.char-count{font-family:var(--mono);font-size:10px;color:var(--muted)}
|
| 118 |
+
|
| 119 |
+
/* ββ right pane ββ */
|
| 120 |
+
.right-pane{display:flex;flex-direction:column;gap:12px;overflow-y:auto;max-height:calc(100vh - 160px)}
|
| 121 |
+
@media(max-width:900px){.right-pane{max-height:none}}
|
| 122 |
+
|
| 123 |
+
/* ββ reward display ββ */
|
| 124 |
+
.reward-big{text-align:center;padding:20px 14px}
|
| 125 |
+
.reward-number{font-family:var(--mono);font-size:52px;font-weight:700;line-height:1;
|
| 126 |
+
transition:all .4s ease}
|
| 127 |
+
.reward-label{font-size:11px;color:var(--muted);font-family:var(--mono);margin-top:4px}
|
| 128 |
+
.reward-bar-bg{height:6px;background:var(--s3);border-radius:99px;margin:12px 0}
|
| 129 |
+
.reward-bar{height:6px;border-radius:99px;background:var(--accent);transition:width .6s ease;width:0%}
|
| 130 |
+
|
| 131 |
+
/* ββ score breakdown ββ */
|
| 132 |
+
.score-row{display:flex;align-items:center;gap:8px;padding:5px 0;
|
| 133 |
+
border-bottom:1px solid var(--border);font-size:12px}
|
| 134 |
+
.score-row:last-child{border:none}
|
| 135 |
+
.score-dim{flex:1;color:var(--muted);font-family:var(--mono)}
|
| 136 |
+
.score-val{font-family:var(--mono);font-weight:700;min-width:38px;text-align:right}
|
| 137 |
+
.score-bar-bg{width:60px;height:4px;background:var(--s3);border-radius:99px}
|
| 138 |
+
.score-bar-fg{height:4px;border-radius:99px;transition:width .5s ease;background:var(--a3)}
|
| 139 |
+
.weight-tag{font-size:9px;color:var(--s3);background:var(--border);
|
| 140 |
+
padding:1px 5px;border-radius:3px;font-family:var(--mono)}
|
| 141 |
+
|
| 142 |
+
/* ββ feedback ββ */
|
| 143 |
+
.fb-item{font-size:11px;font-family:var(--mono);padding:5px 8px;border-radius:5px;
|
| 144 |
+
background:var(--s2);border-left:3px solid var(--border);margin-bottom:4px;line-height:1.5}
|
| 145 |
+
.fb-item.good{border-left-color:var(--a3)}
|
| 146 |
+
.fb-item.warn{border-left-color:var(--warn)}
|
| 147 |
+
.fb-item.bad{border-left-color:var(--danger)}
|
| 148 |
+
|
| 149 |
+
/* ββ history ββ */
|
| 150 |
+
.history-item{display:flex;align-items:center;gap:8px;padding:7px 10px;
|
| 151 |
+
border-bottom:1px solid var(--border);font-size:11px;font-family:var(--mono)}
|
| 152 |
+
.history-item:last-child{border:none}
|
| 153 |
+
.h-step{color:var(--muted);min-width:40px}
|
| 154 |
+
.h-reward{font-weight:700;min-width:50px}
|
| 155 |
+
.h-bar{flex:1;height:4px;background:var(--s3);border-radius:99px;position:relative}
|
| 156 |
+
.h-bar-fg{height:4px;border-radius:99px;background:var(--a3);transition:width .4s}
|
| 157 |
+
.h-done{color:var(--a3);font-size:10px}
|
| 158 |
+
|
| 159 |
+
/* ββ loading ββ */
|
| 160 |
+
.spinner{display:inline-block;width:14px;height:14px;border:2px solid rgba(255,255,255,.2);
|
| 161 |
+
border-top-color:var(--accent);border-radius:50%;animation:spin .6s linear infinite}
|
| 162 |
+
@keyframes spin{to{transform:rotate(360deg)}}
|
| 163 |
+
|
| 164 |
+
/* ββ empty state ββ */
|
| 165 |
+
.empty{text-align:center;padding:40px 20px;color:var(--muted)}
|
| 166 |
+
.empty-icon{font-size:32px;margin-bottom:12px;opacity:.5}
|
| 167 |
+
.empty-text{font-size:13px;line-height:1.6}
|
| 168 |
+
|
| 169 |
+
/* ββ alerts ββ */
|
| 170 |
+
.alert{padding:10px 14px;border-radius:6px;font-size:12px;font-family:var(--mono);
|
| 171 |
+
margin-bottom:8px;display:flex;gap:8px;align-items:flex-start}
|
| 172 |
+
.alert-error{background:rgba(255,123,114,.1);border:1px solid rgba(255,123,114,.3);color:var(--danger)}
|
| 173 |
+
.alert-success{background:rgba(86,211,100,.1);border:1px solid rgba(86,211,100,.3);color:var(--a3)}
|
| 174 |
+
.alert-info{background:rgba(121,192,255,.1);border:1px solid rgba(121,192,255,.3);color:var(--a2)}
|
| 175 |
+
|
| 176 |
+
/* ββ overview panel ββ */
|
| 177 |
+
.grid-2{display:grid;grid-template-columns:repeat(auto-fill,minmax(260px,1fr));gap:12px}
|
| 178 |
+
.stat-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
| 179 |
+
padding:20px;display:flex;flex-direction:column;gap:6px}
|
| 180 |
+
.stat-val{font-family:var(--mono);font-size:36px;font-weight:700;color:var(--accent)}
|
| 181 |
+
.stat-label{font-size:12px;color:var(--muted)}
|
| 182 |
+
.section-label{font-family:var(--mono);font-size:10px;color:var(--muted);letter-spacing:2px;
|
| 183 |
+
text-transform:uppercase;padding:16px 0 8px;border-bottom:1px solid var(--border);margin-bottom:12px}
|
| 184 |
+
|
| 185 |
+
/* ββ task list ββ */
|
| 186 |
+
.task-list-item{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
| 187 |
+
padding:14px 16px;cursor:pointer;transition:border-color .15s;margin-bottom:8px}
|
| 188 |
+
.task-list-item:hover{border-color:var(--accent)}
|
| 189 |
+
.tli-header{display:flex;align-items:center;justify-content:space-between;margin-bottom:6px}
|
| 190 |
+
.tli-name{font-weight:700;font-size:14px}
|
| 191 |
+
.tli-desc{font-size:12px;color:var(--muted);line-height:1.5}
|
| 192 |
+
.tli-footer{display:flex;gap:6px;margin-top:10px;flex-wrap:wrap}
|
| 193 |
+
|
| 194 |
+
/* ββ docs panel ββ */
|
| 195 |
+
.docs-card{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);
|
| 196 |
+
padding:20px;margin-bottom:12px}
|
| 197 |
+
.docs-h2{font-size:16px;font-weight:700;margin-bottom:8px;color:var(--text)}
|
| 198 |
+
.docs-p{font-size:13px;color:var(--muted);line-height:1.7;margin-bottom:12px}
|
| 199 |
+
.docs-code{background:var(--s2);border:1px solid var(--border);border-radius:6px;
|
| 200 |
+
padding:14px;font-family:var(--mono);font-size:12px;line-height:1.65;
|
| 201 |
+
overflow-x:auto;margin-bottom:12px;white-space:pre}
|
| 202 |
+
.method{font-weight:700;font-size:11px;padding:2px 7px;border-radius:4px;font-family:var(--mono)}
|
| 203 |
+
.method.post{background:rgba(86,211,100,.15);color:var(--a3)}
|
| 204 |
+
.method.get{background:rgba(121,192,255,.15);color:var(--a2)}
|
| 205 |
+
.ep-row{display:flex;align-items:flex-start;gap:12px;padding:10px 0;
|
| 206 |
+
border-bottom:1px solid var(--border);font-size:13px}
|
| 207 |
+
.ep-row:last-child{border:none}
|
| 208 |
+
.ep-path{font-family:var(--mono);color:var(--text);font-weight:700;min-width:180px}
|
| 209 |
+
.ep-desc{color:var(--muted);line-height:1.5}
|
| 210 |
+
|
| 211 |
+
/* ββ reward weight chart ββ */
|
| 212 |
+
.weight-bar-row{display:flex;align-items:center;gap:10px;padding:6px 0;font-size:12px}
|
| 213 |
+
.wbr-name{flex:0 0 140px;font-family:var(--mono);color:var(--muted)}
|
| 214 |
+
.wbr-bg{flex:1;height:8px;background:var(--s3);border-radius:99px}
|
| 215 |
+
.wbr-fg{height:8px;border-radius:99px;background:var(--accent);transition:width .8s ease;width:0%}
|
| 216 |
+
.wbr-val{font-family:var(--mono);font-weight:700;color:var(--accent);min-width:36px;text-align:right}
|
| 217 |
+
|
| 218 |
+
/* scrollbar */
|
| 219 |
+
::-webkit-scrollbar{width:6px;height:6px}
|
| 220 |
+
::-webkit-scrollbar-track{background:var(--bg)}
|
| 221 |
+
::-webkit-scrollbar-thumb{background:var(--border);border-radius:3px}
|
| 222 |
+
</style>
|
| 223 |
+
</head>
|
| 224 |
+
<body>
|
| 225 |
+
|
| 226 |
+
<!-- HEADER -->
|
| 227 |
+
<header>
|
| 228 |
+
<div class="hlogo">
|
| 229 |
+
<div class="hlogo-icon">π</div>
|
| 230 |
+
SecureCodeEnv
|
| 231 |
+
</div>
|
| 232 |
+
<div class="hbadges">
|
| 233 |
+
<span class="badge bo">v2.0.0</span>
|
| 234 |
+
<span class="badge bb">OpenEnv</span>
|
| 235 |
+
<span class="badge br">Meta Γ PyTorch Hackathon</span>
|
| 236 |
+
</div>
|
| 237 |
+
<div class="hstatus">
|
| 238 |
+
<div class="dot pulse" id="status-dot"></div>
|
| 239 |
+
<span id="status-text" style="font-size:11px"></span>
|
| 240 |
+
</div>
|
| 241 |
+
</header>
|
| 242 |
+
|
| 243 |
+
<!-- NAV -->
|
| 244 |
+
<nav class="nav">
|
| 245 |
+
<button class="ntab active" onclick="showPanel('playground', this)">β‘ Playground</button>
|
| 246 |
+
<button class="ntab" onclick="showPanel('overview', this)">π Overview</button>
|
| 247 |
+
<button class="ntab" onclick="showPanel('tasks', this)">π Tasks</button>
|
| 248 |
+
<button class="ntab" onclick="showPanel('docs', this)">π API Docs</button>
|
| 249 |
+
</nav>
|
| 250 |
+
|
| 251 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 252 |
+
<!-- PLAYGROUND PANEL -->
|
| 253 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 254 |
+
<div class="main">
|
| 255 |
+
<div id="panel-playground" class="panel active">
|
| 256 |
+
<div class="playground">
|
| 257 |
+
|
| 258 |
+
<!-- LEFT: controls + task + editor -->
|
| 259 |
+
<div class="left-pane">
|
| 260 |
+
|
| 261 |
+
<!-- Episode controls -->
|
| 262 |
+
<div class="card">
|
| 263 |
+
<div class="card-header">
|
| 264 |
+
<span class="card-title">Episode Control</span>
|
| 265 |
+
<span id="session-badge" class="badge bb" style="display:none"></span>
|
| 266 |
+
</div>
|
| 267 |
+
<div class="card-body">
|
| 268 |
+
<div id="alert-area"></div>
|
| 269 |
+
<div class="controls-row">
|
| 270 |
+
<select id="diff-select">
|
| 271 |
+
<option value="easy">Easy</option>
|
| 272 |
+
<option value="medium" selected>Medium</option>
|
| 273 |
+
<option value="hard">Hard</option>
|
| 274 |
+
</select>
|
| 275 |
+
<select id="task-select" style="flex:1">
|
| 276 |
+
<option value="">Random task</option>
|
| 277 |
+
</select>
|
| 278 |
+
<button class="btn btn-primary" id="btn-reset" onclick="doReset()">
|
| 279 |
+
<span id="reset-spinner" style="display:none" class="spinner"></span>
|
| 280 |
+
π Reset
|
| 281 |
+
</button>
|
| 282 |
+
</div>
|
| 283 |
+
<div id="task-area" style="margin-top:12px;display:none">
|
| 284 |
+
<div class="task-meta" id="task-meta"></div>
|
| 285 |
+
<div class="task-box" id="task-box"></div>
|
| 286 |
+
</div>
|
| 287 |
+
</div>
|
| 288 |
+
</div>
|
| 289 |
+
|
| 290 |
+
<!-- Code editor -->
|
| 291 |
+
<div class="card editor-wrap">
|
| 292 |
+
<div class="editor-header">
|
| 293 |
+
<div class="editor-dots"><span></span><span></span><span></span></div>
|
| 294 |
+
<span style="font-family:var(--mono);font-size:11px;color:var(--muted)" id="editor-filename">solution.py</span>
|
| 295 |
+
<div style="display:flex;gap:6px">
|
| 296 |
+
<button class="btn btn-ghost" style="padding:4px 10px;font-size:11px" onclick="loadStarter()">Load starter</button>
|
| 297 |
+
<button class="btn btn-ghost" style="padding:4px 10px;font-size:11px" onclick="clearEditor()">Clear</button>
|
| 298 |
+
</div>
|
| 299 |
+
</div>
|
| 300 |
+
<textarea id="code-editor" spellcheck="false"
|
| 301 |
+
placeholder="# Reset an episode first, then write your Python solution here...
|
| 302 |
+
# Click 'Load starter' to get the buggy starter code to fix.
|
| 303 |
+
|
| 304 |
+
def your_function():
|
| 305 |
+
pass"></textarea>
|
| 306 |
+
<div class="editor-footer">
|
| 307 |
+
<span class="char-count" id="char-count">0 chars</span>
|
| 308 |
+
<div style="display:flex;gap:8px">
|
| 309 |
+
<span id="step-counter" style="font-family:var(--mono);font-size:11px;color:var(--muted)">Step 0/5</span>
|
| 310 |
+
<button class="btn btn-green" id="btn-submit" onclick="doStep()" disabled>
|
| 311 |
+
<span id="submit-spinner" style="display:none" class="spinner"></span>
|
| 312 |
+
βΆ Submit
|
| 313 |
+
</button>
|
| 314 |
+
</div>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</div>
|
| 318 |
+
|
| 319 |
+
<!-- RIGHT: rewards + feedback + history -->
|
| 320 |
+
<div class="right-pane">
|
| 321 |
+
|
| 322 |
+
<!-- Total reward -->
|
| 323 |
+
<div class="card">
|
| 324 |
+
<div class="card-header"><span class="card-title">Total Reward</span><span id="done-badge" style="display:none" class="badge bg">DONE β</span></div>
|
| 325 |
+
<div class="card-body">
|
| 326 |
+
<div class="reward-big">
|
| 327 |
+
<div class="reward-number" id="reward-number" style="color:var(--muted)">β</div>
|
| 328 |
+
<div class="reward-label">/ 1.000 maximum</div>
|
| 329 |
+
</div>
|
| 330 |
+
<div class="reward-bar-bg"><div class="reward-bar" id="reward-bar"></div></div>
|
| 331 |
+
<div id="summary-text" style="font-size:12px;font-family:var(--mono);color:var(--muted);text-align:center"></div>
|
| 332 |
+
</div>
|
| 333 |
+
</div>
|
| 334 |
+
|
| 335 |
+
<!-- Score breakdown -->
|
| 336 |
+
<div class="card">
|
| 337 |
+
<div class="card-header"><span class="card-title">Score Breakdown</span></div>
|
| 338 |
+
<div class="card-body" id="score-breakdown">
|
| 339 |
+
<div class="empty"><div class="empty-icon">π</div><div class="empty-text">Submit code to see scores</div></div>
|
| 340 |
+
</div>
|
| 341 |
+
</div>
|
| 342 |
+
|
| 343 |
+
<!-- Feedback -->
|
| 344 |
+
<div class="card">
|
| 345 |
+
<div class="card-header"><span class="card-title">Feedback</span></div>
|
| 346 |
+
<div class="card-body" id="feedback-area">
|
| 347 |
+
<div class="empty"><div class="empty-icon">π¬</div><div class="empty-text">Feedback will appear here</div></div>
|
| 348 |
+
</div>
|
| 349 |
+
</div>
|
| 350 |
+
|
| 351 |
+
<!-- Step history -->
|
| 352 |
+
<div class="card">
|
| 353 |
+
<div class="card-header"><span class="card-title">Episode History</span><span class="char-count" id="history-count">0 steps</span></div>
|
| 354 |
+
<div id="history-area">
|
| 355 |
+
<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>
|
| 356 |
+
</div>
|
| 357 |
+
</div>
|
| 358 |
+
|
| 359 |
+
</div>
|
| 360 |
+
</div>
|
| 361 |
+
</div>
|
| 362 |
+
|
| 363 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 364 |
+
<!-- OVERVIEW PANEL -->
|
| 365 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 366 |
+
<div id="panel-overview" class="panel">
|
| 367 |
+
<div class="section-label">Environment Stats</div>
|
| 368 |
+
<div class="grid-2">
|
| 369 |
+
<div class="stat-card"><div class="stat-val">9</div><div class="stat-label">Security Tasks (3 per difficulty)</div></div>
|
| 370 |
+
<div class="stat-card"><div class="stat-val">7</div><div class="stat-label">Reward Dimensions</div></div>
|
| 371 |
+
<div class="stat-card"><div class="stat-val">12+</div><div class="stat-label">CWE IDs Covered</div></div>
|
| 372 |
+
<div class="stat-card"><div class="stat-val">$0</div><div class="stat-label">Infrastructure Cost (HF Spaces free tier)</div></div>
|
| 373 |
+
</div>
|
| 374 |
+
|
| 375 |
+
<div class="section-label" style="margin-top:24px">Reward Weights</div>
|
| 376 |
+
<div class="card"><div class="card-body" id="weight-chart"></div></div>
|
| 377 |
+
|
| 378 |
+
<div class="section-label" style="margin-top:24px">What Makes This Unique</div>
|
| 379 |
+
<div class="grid-2">
|
| 380 |
+
<div class="stat-card" style="gap:10px">
|
| 381 |
+
<div style="font-size:22px">βοΈ</div>
|
| 382 |
+
<div style="font-weight:700">Dynamic Attack Grading</div>
|
| 383 |
+
<div class="stat-label">We actually FIRE SQL injection, path traversal, JWT bypass, and XSS payloads at your code β not just static pattern matching. Payloads are seeded-random per episode so agents can't memorise them.</div>
|
| 384 |
+
</div>
|
| 385 |
+
<div class="stat-card" style="gap:10px">
|
| 386 |
+
<div style="font-size:22px">π§ </div>
|
| 387 |
+
<div style="font-weight:700">CodeGraph Memory</div>
|
| 388 |
+
<div class="stat-label">The agent's codebase context grows across steps. Conventions (naming, error handling, type hints) are inferred and enforced β the only RL environment that rewards multi-file consistency.</div>
|
| 389 |
+
</div>
|
| 390 |
+
<div class="stat-card" style="gap:10px">
|
| 391 |
+
<div style="font-size:22px">π―</div>
|
| 392 |
+
<div style="font-weight:700">CWE-Grounded Tasks</div>
|
| 393 |
+
<div class="stat-label">Every task maps to real Common Weakness Enumeration IDs. Grading is 100% automated and deterministic β no LLM judge, no subjectivity.</div>
|
| 394 |
+
</div>
|
| 395 |
+
<div class="stat-card" style="gap:10px">
|
| 396 |
+
<div style="font-size:22px">π</div>
|
| 397 |
+
<div style="font-weight:700">Dense Reward Signal</div>
|
| 398 |
+
<div class="stat-label">7 orthogonal dimensions give partial credit at every step. Agents never get 0.0 on a correct-but-insecure submission β they learn incrementally.</div>
|
| 399 |
+
</div>
|
| 400 |
+
</div>
|
| 401 |
+
</div>
|
| 402 |
+
|
| 403 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 404 |
+
<!-- TASKS PANEL -->
|
| 405 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 406 |
+
<div id="panel-tasks" class="panel">
|
| 407 |
+
<div class="section-label">All 9 Tasks</div>
|
| 408 |
+
<div style="display:flex;gap:8px;margin-bottom:16px">
|
| 409 |
+
<button class="btn btn-ghost" onclick="filterTasks('all')" id="f-all" style="border-color:var(--accent);color:var(--accent)">All</button>
|
| 410 |
+
<button class="btn btn-ghost" onclick="filterTasks('easy')" id="f-easy">Easy</button>
|
| 411 |
+
<button class="btn btn-ghost" onclick="filterTasks('medium')" id="f-medium">Medium</button>
|
| 412 |
+
<button class="btn btn-ghost" onclick="filterTasks('hard')" id="f-hard">Hard</button>
|
| 413 |
+
</div>
|
| 414 |
+
<div id="task-list-container">
|
| 415 |
+
<div class="empty"><div class="spinner" style="margin:0 auto"></div></div>
|
| 416 |
+
</div>
|
| 417 |
+
</div>
|
| 418 |
+
|
| 419 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 420 |
+
<!-- DOCS PANEL -->
|
| 421 |
+
<!-- ββββββββββββββββββββββββββββββββββββββββββββββββββ -->
|
| 422 |
+
<div id="panel-docs" class="panel">
|
| 423 |
+
<div class="docs-card">
|
| 424 |
+
<div class="docs-h2">Quick Start</div>
|
| 425 |
+
<div class="docs-p">This environment implements the OpenEnv API contract. Use the Playground tab for interactive testing, or call the endpoints directly.</div>
|
| 426 |
+
<div class="docs-code">import requests
|
| 427 |
+
|
| 428 |
+
ENV = "https://vishaldhakad-securecodeenv.hf.space"
|
| 429 |
+
|
| 430 |
+
# 1. Start episode
|
| 431 |
+
ep = requests.post(f"{ENV}/reset", json={"difficulty": "medium"}).json()
|
| 432 |
+
sid, task = ep["session_id"], ep["task_id"]
|
| 433 |
+
print(ep["problem_statement"])
|
| 434 |
+
|
| 435 |
+
# 2. Submit code
|
| 436 |
+
result = requests.post(f"{ENV}/step", json={
|
| 437 |
+
"session_id": sid,
|
| 438 |
+
"code": "def build_user_query(u, r):\n return ('SELECT * FROM users WHERE username=%s', (u,))",
|
| 439 |
+
"filename": "solution.py"
|
| 440 |
+
}).json()
|
| 441 |
+
|
| 442 |
+
print(f"reward={result['total_reward']:.3f}")
|
| 443 |
+
print(result["feedback"]["summary"])</div>
|
| 444 |
+
</div>
|
| 445 |
+
|
| 446 |
+
<div class="docs-card">
|
| 447 |
+
<div class="docs-h2">Endpoints</div>
|
| 448 |
+
<div class="ep-row">
|
| 449 |
+
<div class="ep-path"><span class="method get">GET</span> /health</div>
|
| 450 |
+
<div class="ep-desc">Health check. Returns <code>{"status":"ok","tasks_loaded":9}</code></div>
|
| 451 |
+
</div>
|
| 452 |
+
<div class="ep-row">
|
| 453 |
+
<div class="ep-path"><span class="method post">POST</span> /reset</div>
|
| 454 |
+
<div class="ep-desc">Start new episode. Body: <code>{"difficulty":"medium","task_id":"optional"}</code>. Returns task + CodeGraph.</div>
|
| 455 |
+
</div>
|
| 456 |
+
<div class="ep-row">
|
| 457 |
+
<div class="ep-path"><span class="method post">POST</span> /step</div>
|
| 458 |
+
<div class="ep-desc">Submit code. Body: <code>{"session_id":"...","code":"...","filename":"..."}</code>. Returns reward + feedback.</div>
|
| 459 |
+
</div>
|
| 460 |
+
<div class="ep-row">
|
| 461 |
+
<div class="ep-path"><span class="method get">GET</span> /state</div>
|
| 462 |
+
<div class="ep-desc">Get episode state. Query: <code>?session_id=...</code></div>
|
| 463 |
+
</div>
|
| 464 |
+
<div class="ep-row">
|
| 465 |
+
<div class="ep-path"><span class="method get">GET</span> /tasks</div>
|
| 466 |
+
<div class="ep-desc">List tasks. Query: <code>?difficulty=easy</code> (optional filter)</div>
|
| 467 |
+
</div>
|
| 468 |
+
<div class="ep-row">
|
| 469 |
+
<div class="ep-path"><span class="method get">GET</span> /tasks/{id}</div>
|
| 470 |
+
<div class="ep-desc">Full task detail including starter code and security checks</div>
|
| 471 |
+
</div>
|
| 472 |
+
<div class="ep-row">
|
| 473 |
+
<div class="ep-path"><span class="method get">GET</span> /docs</div>
|
| 474 |
+
<div class="ep-desc">Auto-generated Swagger UI (FastAPI)</div>
|
| 475 |
+
</div>
|
| 476 |
+
</div>
|
| 477 |
+
|
| 478 |
+
<div class="docs-card">
|
| 479 |
+
<div class="docs-h2">Reward Dimensions</div>
|
| 480 |
+
<table style="width:100%;font-size:12px;font-family:var(--mono);border-collapse:collapse">
|
| 481 |
+
<tr style="border-bottom:1px solid var(--border);color:var(--muted)">
|
| 482 |
+
<td style="padding:6px 8px">Dimension</td>
|
| 483 |
+
<td style="padding:6px 8px">Weight</td>
|
| 484 |
+
<td style="padding:6px 8px">Tool</td>
|
| 485 |
+
<td style="padding:6px 8px">Measures</td>
|
| 486 |
+
</tr>
|
| 487 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 488 |
+
<td style="padding:6px 8px;color:var(--accent)">correctness</td>
|
| 489 |
+
<td style="padding:6px 8px">30%</td>
|
| 490 |
+
<td style="padding:6px 8px;color:var(--muted)">Custom runner</td>
|
| 491 |
+
<td style="padding:6px 8px;color:var(--muted)">Test cases passed</td>
|
| 492 |
+
</tr>
|
| 493 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 494 |
+
<td style="padding:6px 8px;color:var(--accent)">attack_resist</td>
|
| 495 |
+
<td style="padding:6px 8px">20%</td>
|
| 496 |
+
<td style="padding:6px 8px;color:var(--muted)">Dynamic harness</td>
|
| 497 |
+
<td style="padding:6px 8px;color:var(--muted)">Real attack payloads blocked</td>
|
| 498 |
+
</tr>
|
| 499 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 500 |
+
<td style="padding:6px 8px;color:var(--accent)">static_security</td>
|
| 501 |
+
<td style="padding:6px 8px">15%</td>
|
| 502 |
+
<td style="padding:6px 8px;color:var(--muted)">bandit + AST</td>
|
| 503 |
+
<td style="padding:6px 8px;color:var(--muted)">CWE-mapped vulnerability patterns</td>
|
| 504 |
+
</tr>
|
| 505 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 506 |
+
<td style="padding:6px 8px;color:var(--accent)">consistency</td>
|
| 507 |
+
<td style="padding:6px 8px">15%</td>
|
| 508 |
+
<td style="padding:6px 8px;color:var(--muted)">CodeGraph</td>
|
| 509 |
+
<td style="padding:6px 8px;color:var(--muted)">Codebase convention adherence</td>
|
| 510 |
+
</tr>
|
| 511 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 512 |
+
<td style="padding:6px 8px;color:var(--accent)">performance</td>
|
| 513 |
+
<td style="padding:6px 8px">10%</td>
|
| 514 |
+
<td style="padding:6px 8px;color:var(--muted)">timeit</td>
|
| 515 |
+
<td style="padding:6px 8px;color:var(--muted)">Speed vs naive/optimal baselines</td>
|
| 516 |
+
</tr>
|
| 517 |
+
<tr style="border-bottom:1px solid var(--border)">
|
| 518 |
+
<td style="padding:6px 8px;color:var(--accent)">documentation</td>
|
| 519 |
+
<td style="padding:6px 8px">5%</td>
|
| 520 |
+
<td style="padding:6px 8px;color:var(--muted)">AST</td>
|
| 521 |
+
<td style="padding:6px 8px;color:var(--muted)">Docstrings + type hints coverage</td>
|
| 522 |
+
</tr>
|
| 523 |
+
<tr>
|
| 524 |
+
<td style="padding:6px 8px;color:var(--accent)">code_structure</td>
|
| 525 |
+
<td style="padding:6px 8px">5%</td>
|
| 526 |
+
<td style="padding:6px 8px;color:var(--muted)">AST</td>
|
| 527 |
+
<td style="padding:6px 8px;color:var(--muted)">No bare print/except, clean structure</td>
|
| 528 |
+
</tr>
|
| 529 |
+
</table>
|
| 530 |
+
</div>
|
| 531 |
+
</div>
|
| 532 |
+
</div><!-- /main -->
|
| 533 |
+
|
| 534 |
+
<script>
|
| 535 |
+
// ββ State ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 536 |
+
const state = {
|
| 537 |
+
sessionId: null,
|
| 538 |
+
task: null,
|
| 539 |
+
stepCount: 0,
|
| 540 |
+
done: false,
|
| 541 |
+
history: [],
|
| 542 |
+
allTasks: [],
|
| 543 |
+
};
|
| 544 |
+
|
| 545 |
+
const WEIGHTS = {
|
| 546 |
+
correctness:0.30, attack_resist:0.20, static_security:0.15,
|
| 547 |
+
consistency:0.15, performance:0.10, documentation:0.05, code_structure:0.05
|
| 548 |
+
};
|
| 549 |
+
|
| 550 |
+
// ββ Init βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 551 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 552 |
+
checkHealth();
|
| 553 |
+
loadTasksDropdown();
|
| 554 |
+
renderWeightChart();
|
| 555 |
+
document.getElementById('code-editor').addEventListener('input', updateCharCount);
|
| 556 |
+
updateCharCount();
|
| 557 |
+
});
|
| 558 |
+
|
| 559 |
+
// ββ Health check βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 560 |
+
async function checkHealth() {
|
| 561 |
+
const dot = document.getElementById('status-dot');
|
| 562 |
+
const txt = document.getElementById('status-text');
|
| 563 |
+
try {
|
| 564 |
+
const r = await fetch('/health');
|
| 565 |
+
const d = await r.json();
|
| 566 |
+
dot.className = 'dot pulse';
|
| 567 |
+
txt.textContent = `${d.env} v${d.version} Β· ${d.tasks_loaded} tasks`;
|
| 568 |
+
} catch(e) {
|
| 569 |
+
dot.className = 'dot red';
|
| 570 |
+
txt.textContent = 'Environment unreachable';
|
| 571 |
+
}
|
| 572 |
+
}
|
| 573 |
+
|
| 574 |
+
// ββ Tab navigation βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 575 |
+
function showPanel(id, btn) {
|
| 576 |
+
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
|
| 577 |
+
document.querySelectorAll('.ntab').forEach(t => t.classList.remove('active'));
|
| 578 |
+
document.getElementById('panel-'+id).classList.add('active');
|
| 579 |
+
btn.classList.add('active');
|
| 580 |
+
if (id === 'tasks' && state.allTasks.length === 0) loadTasksList();
|
| 581 |
+
}
|
| 582 |
+
|
| 583 |
+
// ββ Task dropdown ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 584 |
+
async function loadTasksDropdown() {
|
| 585 |
+
try {
|
| 586 |
+
const r = await fetch('/tasks');
|
| 587 |
+
const tasks = await r.json();
|
| 588 |
+
state.allTasks = tasks;
|
| 589 |
+
const sel = document.getElementById('task-select');
|
| 590 |
+
tasks.forEach(t => {
|
| 591 |
+
const opt = document.createElement('option');
|
| 592 |
+
opt.value = t.id;
|
| 593 |
+
opt.textContent = `${t.id.replace(/_/g,' ')}`;
|
| 594 |
+
sel.appendChild(opt);
|
| 595 |
+
});
|
| 596 |
+
} catch(e) {}
|
| 597 |
+
}
|
| 598 |
+
|
| 599 |
+
// ββ Reset episode ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 600 |
+
async function doReset() {
|
| 601 |
+
const btn = document.getElementById('btn-reset');
|
| 602 |
+
const spin = document.getElementById('reset-spinner');
|
| 603 |
+
btn.disabled = true; spin.style.display = 'inline-block';
|
| 604 |
+
clearAlert();
|
| 605 |
+
|
| 606 |
+
const difficulty = document.getElementById('diff-select').value;
|
| 607 |
+
const taskId = document.getElementById('task-select').value;
|
| 608 |
+
|
| 609 |
+
try {
|
| 610 |
+
const body = { difficulty };
|
| 611 |
+
if (taskId) body.task_id = taskId;
|
| 612 |
+
const r = await fetch('/reset', {
|
| 613 |
+
method: 'POST',
|
| 614 |
+
headers: {'Content-Type':'application/json'},
|
| 615 |
+
body: JSON.stringify(body)
|
| 616 |
+
});
|
| 617 |
+
if (!r.ok) {
|
| 618 |
+
const e = await r.json();
|
| 619 |
+
showAlert(e.detail || 'Reset failed', 'error');
|
| 620 |
+
return;
|
| 621 |
+
}
|
| 622 |
+
const d = await r.json();
|
| 623 |
+
state.sessionId = d.session_id;
|
| 624 |
+
state.task = d;
|
| 625 |
+
state.stepCount = 0;
|
| 626 |
+
state.done = false;
|
| 627 |
+
state.history = [];
|
| 628 |
+
|
| 629 |
+
renderTask(d);
|
| 630 |
+
resetResultPanel();
|
| 631 |
+
updateStepCounter();
|
| 632 |
+
document.getElementById('btn-submit').disabled = false;
|
| 633 |
+
document.getElementById('session-badge').style.display = 'inline';
|
| 634 |
+
document.getElementById('session-badge').textContent = d.session_id.slice(0,8) + 'β¦';
|
| 635 |
+
showAlert(`β Episode started: ${d.task_id}`, 'success');
|
| 636 |
+
} catch(e) {
|
| 637 |
+
showAlert('Network error: ' + e.message, 'error');
|
| 638 |
+
} finally {
|
| 639 |
+
btn.disabled = false; spin.style.display = 'none';
|
| 640 |
+
}
|
| 641 |
+
}
|
| 642 |
+
|
| 643 |
+
// ββ Submit step ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 644 |
+
async function doStep() {
|
| 645 |
+
if (!state.sessionId) { showAlert('Reset an episode first', 'error'); return; }
|
| 646 |
+
const code = document.getElementById('code-editor').value.trim();
|
| 647 |
+
if (!code) { showAlert('Write some code first', 'error'); return; }
|
| 648 |
+
|
| 649 |
+
const btn = document.getElementById('btn-submit');
|
| 650 |
+
const spin = document.getElementById('submit-spinner');
|
| 651 |
+
btn.disabled = true; spin.style.display = 'inline-block';
|
| 652 |
+
clearAlert();
|
| 653 |
+
|
| 654 |
+
try {
|
| 655 |
+
const r = await fetch('/step', {
|
| 656 |
+
method: 'POST',
|
| 657 |
+
headers: {'Content-Type':'application/json'},
|
| 658 |
+
body: JSON.stringify({
|
| 659 |
+
session_id: state.sessionId,
|
| 660 |
+
code,
|
| 661 |
+
filename: `solution_step${state.stepCount}.py`
|
| 662 |
+
})
|
| 663 |
+
});
|
| 664 |
+
if (!r.ok) {
|
| 665 |
+
const e = await r.json();
|
| 666 |
+
showAlert(e.detail || 'Step failed', 'error');
|
| 667 |
+
if (r.status === 400 && e.detail.includes('done')) {
|
| 668 |
+
btn.disabled = true;
|
| 669 |
+
}
|
| 670 |
+
return;
|
| 671 |
+
}
|
| 672 |
+
const d = await r.json();
|
| 673 |
+
state.stepCount = d.step_count;
|
| 674 |
+
state.done = d.done;
|
| 675 |
+
state.history.push({ step: d.step_count, reward: d.total_reward, done: d.done });
|
| 676 |
+
|
| 677 |
+
renderReward(d.total_reward);
|
| 678 |
+
renderScores(d.scores, d.details);
|
| 679 |
+
renderFeedback(d.feedback);
|
| 680 |
+
renderHistory();
|
| 681 |
+
updateStepCounter();
|
| 682 |
+
|
| 683 |
+
if (d.done) {
|
| 684 |
+
btn.disabled = true;
|
| 685 |
+
document.getElementById('done-badge').style.display = 'inline';
|
| 686 |
+
const msg = d.total_reward >= 0.9
|
| 687 |
+
? 'π Excellent! Episode solved!'
|
| 688 |
+
: `Episode complete after ${d.step_count} steps`;
|
| 689 |
+
showAlert(msg, d.total_reward >= 0.9 ? 'success' : 'info');
|
| 690 |
+
}
|
| 691 |
+
} catch(e) {
|
| 692 |
+
showAlert('Network error: ' + e.message, 'error');
|
| 693 |
+
} finally {
|
| 694 |
+
if (!state.done) btn.disabled = false;
|
| 695 |
+
spin.style.display = 'none';
|
| 696 |
+
}
|
| 697 |
+
}
|
| 698 |
+
|
| 699 |
+
// ββ Render helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 700 |
+
function renderTask(d) {
|
| 701 |
+
const area = document.getElementById('task-area');
|
| 702 |
+
area.style.display = 'block';
|
| 703 |
+
|
| 704 |
+
const meta = document.getElementById('task-meta');
|
| 705 |
+
const diffClass = d.difficulty;
|
| 706 |
+
meta.innerHTML = `<span class="diff-tag ${diffClass}">${d.difficulty}</span>`
|
| 707 |
+
+ d.cwe_targets.map(c => `<span class="cwe">${c}</span>`).join('');
|
| 708 |
+
|
| 709 |
+
document.getElementById('task-box').textContent = d.problem_statement;
|
| 710 |
+
document.getElementById('editor-filename').textContent =
|
| 711 |
+
state.allTasks.find(t => t.id === d.task_id)?.id?.replace('_','/')+'.py' || 'solution.py';
|
| 712 |
+
}
|
| 713 |
+
|
| 714 |
+
function renderReward(reward) {
|
| 715 |
+
const n = document.getElementById('reward-number');
|
| 716 |
+
const bar = document.getElementById('reward-bar');
|
| 717 |
+
n.textContent = reward.toFixed(3);
|
| 718 |
+
n.style.color = reward >= 0.9 ? 'var(--a3)' : reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
|
| 719 |
+
bar.style.width = (reward * 100) + '%';
|
| 720 |
+
bar.style.background = reward >= 0.9 ? 'var(--a3)' : reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
|
| 721 |
+
}
|
| 722 |
+
|
| 723 |
+
function renderScores(scores, details) {
|
| 724 |
+
const el = document.getElementById('score-breakdown');
|
| 725 |
+
const rows = Object.entries(scores).map(([k, v]) => {
|
| 726 |
+
const pct = Math.round(v * 100);
|
| 727 |
+
const color = v >= 0.8 ? 'var(--a3)' : v >= 0.5 ? 'var(--accent)' : 'var(--danger)';
|
| 728 |
+
const w = Math.round(WEIGHTS[k] * 100);
|
| 729 |
+
let extra = '';
|
| 730 |
+
if (details) {
|
| 731 |
+
if (k === 'correctness' && details.correctness_total) {
|
| 732 |
+
extra = ` (${details.correctness_passed}/${details.correctness_total})`;
|
| 733 |
+
} else if (k === 'attack_resist' && details.attacks_total) {
|
| 734 |
+
extra = ` (${details.attacks_blocked}/${details.attacks_total} blocked)`;
|
| 735 |
+
}
|
| 736 |
+
}
|
| 737 |
+
return `<div class="score-row">
|
| 738 |
+
<div class="score-dim">${k}${extra}</div>
|
| 739 |
+
<div class="score-bar-bg"><div class="score-bar-fg" style="width:${pct}%;background:${color}"></div></div>
|
| 740 |
+
<div class="score-val" style="color:${color}">${v.toFixed(2)}</div>
|
| 741 |
+
<div class="weight-tag">${w}%</div>
|
| 742 |
+
</div>`;
|
| 743 |
+
});
|
| 744 |
+
el.innerHTML = rows.join('');
|
| 745 |
+
document.getElementById('summary-text').textContent = '';
|
| 746 |
+
}
|
| 747 |
+
|
| 748 |
+
function renderFeedback(feedback) {
|
| 749 |
+
const el = document.getElementById('feedback-area');
|
| 750 |
+
const summary = feedback.summary || '';
|
| 751 |
+
const items = Object.entries(feedback).filter(([k]) => k !== 'summary');
|
| 752 |
+
const good = (v) => v.startsWith('Excellent') || v.startsWith('Clean') || v.startsWith('Well');
|
| 753 |
+
const bad = (v) => v.includes('Poor') || v.includes('Vulnerable') || v.includes('major') || v.includes('HIGH');
|
| 754 |
+
|
| 755 |
+
const html = `<div class="fb-item ${summary.includes('β
') ? 'good' : summary.includes('π΄') ? 'bad' : 'warn'}">${escHtml(summary)}</div>`
|
| 756 |
+
+ items.map(([k, v]) => {
|
| 757 |
+
const cls = good(v) ? 'good' : bad(v) ? 'bad' : 'warn';
|
| 758 |
+
return `<div class="fb-item ${cls}"><strong>${k}:</strong> ${escHtml(v)}</div>`;
|
| 759 |
+
}).join('');
|
| 760 |
+
el.innerHTML = html;
|
| 761 |
+
}
|
| 762 |
+
|
| 763 |
+
function renderHistory() {
|
| 764 |
+
const el = document.getElementById('history-area');
|
| 765 |
+
const count = document.getElementById('history-count');
|
| 766 |
+
count.textContent = `${state.history.length} steps`;
|
| 767 |
+
if (!state.history.length) { el.innerHTML = '<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>'; return; }
|
| 768 |
+
el.innerHTML = state.history.map(h => {
|
| 769 |
+
const color = h.reward >= 0.9 ? 'var(--a3)' : h.reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
|
| 770 |
+
return `<div class="history-item">
|
| 771 |
+
<span class="h-step">Step ${h.step}</span>
|
| 772 |
+
<span class="h-reward" style="color:${color}">${h.reward.toFixed(3)}</span>
|
| 773 |
+
<div class="h-bar"><div class="h-bar-fg" style="width:${h.reward*100}%;background:${color}"></div></div>
|
| 774 |
+
${h.done ? '<span class="h-done">done</span>' : ''}
|
| 775 |
+
</div>`;
|
| 776 |
+
}).join('');
|
| 777 |
+
}
|
| 778 |
+
|
| 779 |
+
function resetResultPanel() {
|
| 780 |
+
document.getElementById('reward-number').textContent = 'β';
|
| 781 |
+
document.getElementById('reward-number').style.color = 'var(--muted)';
|
| 782 |
+
document.getElementById('reward-bar').style.width = '0%';
|
| 783 |
+
document.getElementById('score-breakdown').innerHTML = '<div class="empty"><div class="empty-icon">π</div><div class="empty-text">Submit code to see scores</div></div>';
|
| 784 |
+
document.getElementById('feedback-area').innerHTML = '<div class="empty"><div class="empty-icon">π¬</div><div class="empty-text">Feedback will appear here</div></div>';
|
| 785 |
+
document.getElementById('history-area').innerHTML = '<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>';
|
| 786 |
+
document.getElementById('history-count').textContent = '0 steps';
|
| 787 |
+
document.getElementById('done-badge').style.display = 'none';
|
| 788 |
+
document.getElementById('summary-text').textContent = '';
|
| 789 |
+
}
|
| 790 |
+
|
| 791 |
+
function updateStepCounter() {
|
| 792 |
+
document.getElementById('step-counter').textContent = `Step ${state.stepCount}/5`;
|
| 793 |
+
}
|
| 794 |
+
|
| 795 |
+
function updateCharCount() {
|
| 796 |
+
const len = document.getElementById('code-editor').value.length;
|
| 797 |
+
document.getElementById('char-count').textContent = `${len} chars`;
|
| 798 |
+
}
|
| 799 |
+
|
| 800 |
+
// ββ Editor helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 801 |
+
async function loadStarter() {
|
| 802 |
+
if (!state.task) { showAlert('Reset an episode first', 'error'); return; }
|
| 803 |
+
const tid = state.task.task_id;
|
| 804 |
+
try {
|
| 805 |
+
const r = await fetch(`/tasks/${tid}`);
|
| 806 |
+
const d = await r.json();
|
| 807 |
+
if (d.starter_code) {
|
| 808 |
+
document.getElementById('code-editor').value = d.starter_code;
|
| 809 |
+
updateCharCount();
|
| 810 |
+
}
|
| 811 |
+
} catch(e) {}
|
| 812 |
+
}
|
| 813 |
+
|
| 814 |
+
function clearEditor() {
|
| 815 |
+
document.getElementById('code-editor').value = '';
|
| 816 |
+
updateCharCount();
|
| 817 |
+
}
|
| 818 |
+
|
| 819 |
+
// ββ Alert ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 820 |
+
function showAlert(msg, type='info') {
|
| 821 |
+
const el = document.getElementById('alert-area');
|
| 822 |
+
const cls = type === 'error' ? 'alert-error' : type === 'success' ? 'alert-success' : 'alert-info';
|
| 823 |
+
el.innerHTML = `<div class="alert ${cls}">${escHtml(msg)}</div>`;
|
| 824 |
+
setTimeout(() => { el.innerHTML = ''; }, 5000);
|
| 825 |
+
}
|
| 826 |
+
function clearAlert() { document.getElementById('alert-area').innerHTML = ''; }
|
| 827 |
+
|
| 828 |
+
// ββ Tasks list βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 829 |
+
let taskFilter = 'all';
|
| 830 |
+
async function loadTasksList() {
|
| 831 |
+
if (state.allTasks.length === 0) {
|
| 832 |
+
const r = await fetch('/tasks');
|
| 833 |
+
state.allTasks = await r.json();
|
| 834 |
+
}
|
| 835 |
+
filterTasks('all');
|
| 836 |
+
}
|
| 837 |
+
|
| 838 |
+
function filterTasks(diff) {
|
| 839 |
+
taskFilter = diff;
|
| 840 |
+
['all','easy','medium','hard'].forEach(d => {
|
| 841 |
+
document.getElementById('f-'+d).style.borderColor = '';
|
| 842 |
+
document.getElementById('f-'+d).style.color = '';
|
| 843 |
+
});
|
| 844 |
+
document.getElementById('f-'+diff).style.borderColor = 'var(--accent)';
|
| 845 |
+
document.getElementById('f-'+diff).style.color = 'var(--accent)';
|
| 846 |
+
|
| 847 |
+
const tasks = diff === 'all' ? state.allTasks : state.allTasks.filter(t => t.difficulty === diff);
|
| 848 |
+
const el = document.getElementById('task-list-container');
|
| 849 |
+
if (!tasks.length) { el.innerHTML = '<div class="empty"><div class="empty-text">No tasks found</div></div>'; return; }
|
| 850 |
+
el.innerHTML = tasks.map(t => `
|
| 851 |
+
<div class="task-list-item" onclick="tryTask('${t.id}')">
|
| 852 |
+
<div class="tli-header">
|
| 853 |
+
<div class="tli-name">${t.id.replace(/_/g,' ')}</div>
|
| 854 |
+
<span class="diff-tag ${t.difficulty}">${t.difficulty}</span>
|
| 855 |
+
</div>
|
| 856 |
+
<div class="tli-desc">${escHtml((t.description||'').slice(0,100))}${t.description?.length > 100 ? 'β¦' : ''}</div>
|
| 857 |
+
<div class="tli-footer">
|
| 858 |
+
${t.cwe_targets.map(c => `<span class="cwe">${c}</span>`).join('')}
|
| 859 |
+
<span class="badge bo" style="font-size:9px;margin-left:auto">Try it β</span>
|
| 860 |
+
</div>
|
| 861 |
+
</div>
|
| 862 |
+
`).join('');
|
| 863 |
+
}
|
| 864 |
+
|
| 865 |
+
function tryTask(taskId) {
|
| 866 |
+
showPanel('playground', document.querySelector('.ntab'));
|
| 867 |
+
document.querySelectorAll('.ntab')[0].click();
|
| 868 |
+
document.getElementById('task-select').value = taskId;
|
| 869 |
+
doReset();
|
| 870 |
+
}
|
| 871 |
+
|
| 872 |
+
// ββ Weight chart βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 873 |
+
function renderWeightChart() {
|
| 874 |
+
const el = document.getElementById('weight-chart');
|
| 875 |
+
const entries = [
|
| 876 |
+
['correctness', 0.30], ['attack_resist', 0.20],
|
| 877 |
+
['static_security', 0.15], ['consistency', 0.15],
|
| 878 |
+
['performance', 0.10], ['documentation', 0.05], ['code_structure', 0.05]
|
| 879 |
+
];
|
| 880 |
+
el.innerHTML = entries.map(([name, w]) => `
|
| 881 |
+
<div class="weight-bar-row">
|
| 882 |
+
<div class="wbr-name">${name}</div>
|
| 883 |
+
<div class="wbr-bg"><div class="wbr-fg" style="width:${w*100*3.33}%"></div></div>
|
| 884 |
+
<div class="wbr-val">${Math.round(w*100)}%</div>
|
| 885 |
+
</div>
|
| 886 |
+
`).join('');
|
| 887 |
+
setTimeout(() => {
|
| 888 |
+
document.querySelectorAll('.wbr-fg').forEach(b => {
|
| 889 |
+
b.style.transition = 'width .8s ease';
|
| 890 |
+
});
|
| 891 |
+
}, 100);
|
| 892 |
+
}
|
| 893 |
+
|
| 894 |
+
// ββ Utils ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 895 |
+
function escHtml(s) {
|
| 896 |
+
return String(s||'').replace(/&/g,'&').replace(/</g,'<').replace(/>/g,'>').replace(/"/g,'"');
|
| 897 |
+
}
|
| 898 |
+
|
| 899 |
+
// Tab key in textarea
|
| 900 |
+
document.addEventListener('keydown', e => {
|
| 901 |
+
if (e.target.id === 'code-editor' && e.key === 'Tab') {
|
| 902 |
+
e.preventDefault();
|
| 903 |
+
const s = e.target.selectionStart, en = e.target.selectionEnd;
|
| 904 |
+
e.target.value = e.target.value.substring(0, s) + ' ' + e.target.value.substring(en);
|
| 905 |
+
e.target.selectionStart = e.target.selectionEnd = s + 4;
|
| 906 |
+
updateCharCount();
|
| 907 |
+
}
|
| 908 |
+
// Ctrl+Enter to submit
|
| 909 |
+
if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') doStep();
|
| 910 |
+
});
|
| 911 |
+
</script>
|
| 912 |
+
</body>
|
| 913 |
+
</html>
|