vishaldhakad commited on
Commit
7abab00
Β·
1 Parent(s): 87f5919

removing unnescearry things

Browse files
Files changed (3) hide show
  1. README.md +66 -139
  2. app/dashboard.py +209 -243
  3. dashboard.html +913 -0
README.md CHANGED
@@ -2,7 +2,7 @@
2
  title: Trainx
3
  emoji: πŸ”’
4
  colorFrom: red
5
- colorTo: blue
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** (2025 research). Secure-pass@1 rates remain below 12% for all frontier models even when functional pass@1 exceeds 50%.
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 fills that gap.
26
 
27
  ---
28
 
29
- ## What Makes This Environment Unique
30
 
31
- | Feature | SecureCodeEnv | Other RL Envs |
32
  |---|---|---|
33
- | Dynamic adversarial grading | βœ… Actually FIRES attacks | ❌ Static patterns only |
34
- | CodeGraph memory | βœ… Codebase-consistency rewards | ❌ Single-function only |
35
  | CWE-grounded tasks | βœ… 9 tasks, 12+ CWE IDs | ❌ Generic correctness |
36
- | Multi-dimensional reward | βœ… 7 dimensions | ❌ Pass/fail only |
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 (7 Dimensions)
56
 
57
  | Dimension | Weight | Tool | What It Measures |
58
  |---|---|---|---|
59
- | Correctness | 30% | Custom test runner | Does the code solve the problem? |
60
- | Attack Resistance | 20% | Dynamic harness | Does it survive real attacks? |
61
- | Static Security | 15% | bandit + AST | Known vulnerability patterns (CWE-mapped) |
62
- | CodeGraph Consistency | 15% | AST + CodeGraph | Matches existing codebase conventions? |
63
- | Performance | 10% | timeit + tracemalloc | Efficient vs naive/optimal baselines |
64
- | Documentation | 5% | AST | Docstrings + type hints coverage |
65
- | Code Structure | 5% | AST | Clean code (no bare print, no bare except) |
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 | Attack |
99
- |---|---|---|
100
- | Password Validator | CWE-916, CWE-521 | Weak hash detection |
101
- | Input Sanitizer | CWE-20, CWE-116 | XSS payload injection |
102
- | Token Generator | CWE-338, CWE-330 | Predictable randomness |
103
 
104
  ### Medium
105
- | Task | CWE Targets | Attack |
106
- |---|---|---|
107
- | SQL Query Builder | CWE-89 | SQL injection payloads |
108
- | File Path Handler | CWE-22 | Path traversal attacks |
109
- | Rate Limiter | CWE-770, CWE-400 | Concurrent request flood |
110
 
111
  ### Hard
112
- | Task | CWE Targets | Attack |
113
- |---|---|---|
114
- | File Upload Handler | CWE-22, CWE-434 | Traversal filenames + MIME spoofing |
115
- | JWT Validator | CWE-347, CWE-613 | `alg:none` attack, expired tokens |
116
- | Auth Middleware | CWE-287, CWE-352 | CSRF bypass, timing attacks |
117
 
118
  ---
119
 
120
- ## API Reference
121
 
122
- ### `POST /reset`
123
- Start a new episode.
124
 
125
- **Request:**
126
- ```json
127
- { "difficulty": "medium" }
128
- ```
129
 
130
- **Response:**
131
- ```json
132
- {
133
- "session_id": "uuid",
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
- ### `POST /step`
144
- Submit agent code for grading.
 
 
 
 
145
 
146
- **Request:**
147
- ```json
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
- **Response:**
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
- ### `GET /state?session_id=<id>`
176
- Get current episode state without advancing.
177
 
178
- ### `GET /health`
179
- Returns `{"status": "ok", "env": "SecureCodeEnv", "version": "2.0.0", "tasks_loaded": 9}`
 
 
 
 
 
 
 
180
 
181
  ---
182
 
183
- ## Setup (Local)
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
- # Or direct
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=hf_your_token
204
  export ENV_URL=http://localhost:7860
205
  python inference.py
206
  ```
207
 
208
- ## Validate Before Submit
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 (OpenAI-compatible) |
221
- | `MODEL_NAME` | Yes | Model identifier (e.g. `gpt-4o-mini`) |
222
- | `HF_TOKEN` | Yes | HuggingFace token |
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;color:var(--muted)}
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:180px;
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;position:relative}
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
- padding:20px;margin-bottom:12px}
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
- /* ── reward weight chart ── */
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">OpenEnv</span>
237
- <span class="badge br">Meta Γ— PyTorch Hackathon</span>
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: controls + task + editor -->
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="# Reset an episode first, then write your Python solution here...
304
- # Click 'Load starter' to get the buggy starter code to fix.
 
 
 
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: rewards + feedback + history -->
322
  <div class="right-pane">
323
 
324
- <!-- Total reward -->
325
  <div class="card">
326
- <div class="card-header"><span class="card-title">Total Reward</span><span id="done-badge" style="display:none" class="badge bg">DONE βœ“</span></div>
 
 
 
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"><span class="card-title">Episode History</span><span class="char-count" id="history-count">0 steps</span></div>
 
 
 
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 (3 per difficulty)</div></div>
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">$0</div><div class="stat-label">Infrastructure Cost (HF Spaces free tier)</div></div>
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">What Makes This Unique</div>
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">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>
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 grows across steps. Conventions (naming, error handling, type hints) are inferred and enforced β€” the only RL environment that rewards multi-file consistency.</div>
391
  </div>
392
  <div class="stat-card" style="gap:10px">
393
- <div style="font-size:22px">🎯</div>
394
- <div style="font-weight:700">CWE-Grounded Tasks</div>
395
- <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>
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 orthogonal dimensions give partial credit at every step. Agents never get 0.0 on a correct-but-insecure submission β€” they learn incrementally.</div>
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 9 Tasks</div>
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">This environment implements the OpenEnv API contract. Use the Playground tab for interactive testing, or call the endpoints directly.</div>
428
  <div class="docs-code">import requests
429
 
430
- ENV = "https://vishaldhakad-securecodeenv.hf.space"
431
 
432
  # 1. Start episode
433
- ep = requests.post(f"{ENV}/reset", json={"difficulty": "medium"}).json()
434
- sid, task = ep["session_id"], ep["task_id"]
435
  print(ep["problem_statement"])
436
 
437
- # 2. Submit code
438
- result = requests.post(f"{ENV}/step", json={
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> /health</div>
452
- <div class="ep-desc">Health check. Returns <code>{"status":"ok","tasks_loaded":9}</code></div>
453
  </div>
454
  <div class="ep-row">
455
- <div class="ep-path"><span class="method post">POST</span> /reset</div>
456
- <div class="ep-desc">Start new episode. Body: <code>{"difficulty":"medium","task_id":"optional"}</code>. Returns task + CodeGraph.</div>
457
  </div>
458
  <div class="ep-row">
459
- <div class="ep-path"><span class="method post">POST</span> /step</div>
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> /state</div>
464
- <div class="ep-desc">Get episode state. Query: <code>?session_id=...</code></div>
465
  </div>
466
  <div class="ep-row">
467
- <div class="ep-path"><span class="method get">GET</span> /tasks</div>
468
- <div class="ep-desc">List tasks. Query: <code>?difficulty=easy</code> (optional filter)</div>
469
  </div>
470
  <div class="ep-row">
471
- <div class="ep-path"><span class="method get">GET</span> /tasks/{id}</div>
472
- <div class="ep-desc">Full task detail including starter code and security checks</div>
473
  </div>
474
  <div class="ep-row">
475
- <div class="ep-path"><span class="method get">GET</span> /docs</div>
476
- <div class="ep-desc">Auto-generated Swagger UI (FastAPI)</div>
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">30%</td>
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">20%</td>
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">15%</td>
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">15%</td>
510
  <td style="padding:6px 8px;color:var(--muted)">CodeGraph</td>
511
- <td style="padding:6px 8px;color:var(--muted)">Codebase convention adherence</td>
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">10%</td>
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">5%</td>
522
  <td style="padding:6px 8px;color:var(--muted)">AST</td>
523
- <td style="padding:6px 8px;color:var(--muted)">Docstrings + type hints coverage</td>
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
- task: null,
541
- stepCount: 0,
542
- done: false,
543
- history: [],
544
- allTasks: [],
545
  };
546
 
547
  const WEIGHTS = {
548
- correctness:0.30, attack_resist:0.20, static_security:0.15,
549
- consistency:0.15, performance:0.10, documentation:0.05, code_structure:0.05
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 r = await fetch('/tasks');
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 = `${t.id.replace(/_/g,' ')}`;
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: 'POST',
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: ' + e.message, '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: 'POST',
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 && e.detail.includes('done')) {
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({ step: d.step_count, reward: d.total_reward, done: d.done });
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
- const msg = d.total_reward >= 0.9
689
- ? 'πŸŽ‰ Excellent! Episode solved!'
690
- : `Episode complete after ${d.step_count} steps`;
691
- showAlert(msg, d.total_reward >= 0.9 ? 'success' : 'info');
692
  }
693
  } catch(e) {
694
- showAlert('Network error: ' + e.message, '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
- const area = document.getElementById('task-area');
704
- area.style.display = 'block';
705
-
706
- const meta = document.getElementById('task-meta');
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
- n.style.color = reward >= 0.9 ? 'var(--a3)' : reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
721
- bar.style.width = (reward * 100) + '%';
722
- bar.style.background = reward >= 0.9 ? 'var(--a3)' : reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
 
723
  }
724
 
725
  function renderScores(scores, details) {
726
- const el = document.getElementById('score-breakdown');
727
- const rows = Object.entries(scores).map(([k, v]) => {
728
- const pct = Math.round(v * 100);
729
- const color = v >= 0.8 ? 'var(--a3)' : v >= 0.5 ? 'var(--accent)' : 'var(--danger)';
730
- const w = Math.round(WEIGHTS[k] * 100);
731
  let extra = '';
732
  if (details) {
733
- if (k === 'correctness' && details.correctness_total) {
734
  extra = ` (${details.correctness_passed}/${details.correctness_total})`;
735
- } else if (k === 'attack_resist' && details.attacks_total) {
736
- extra = ` (${details.attacks_blocked}/${details.attacks_total} blocked)`;
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
- el.innerHTML = rows.join('');
747
- document.getElementById('summary-text').textContent = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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]) => k !== 'summary');
754
- const good = (v) => v.startsWith('Excellent') || v.startsWith('Clean') || v.startsWith('Well');
755
- const bad = (v) => v.includes('Poor') || v.includes('Vulnerable') || v.includes('major') || v.includes('HIGH');
756
-
757
- const html = `<div class="fb-item ${summary.includes('βœ…') ? 'good' : summary.includes('πŸ”΄') ? 'bad' : 'warn'}">${escHtml(summary)}</div>`
758
- + items.map(([k, v]) => {
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 el = document.getElementById('history-area');
767
- const count = document.getElementById('history-count');
768
- count.textContent = `${state.history.length} steps`;
769
- if (!state.history.length) { el.innerHTML = '<div class="empty" style="padding:20px"><div class="empty-text">No submissions yet</div></div>'; return; }
770
- el.innerHTML = state.history.map(h => {
771
- const color = h.reward >= 0.9 ? 'var(--a3)' : h.reward >= 0.6 ? 'var(--accent)' : 'var(--danger)';
772
- return `<div class="history-item">
773
- <span class="h-step">Step ${h.step}</span>
774
- <span class="h-reward" style="color:${color}">${h.reward.toFixed(3)}</span>
775
- <div class="h-bar"><div class="h-bar-fg" style="width:${h.reward*100}%;background:${color}"></div></div>
776
- ${h.done ? '<span class="h-done">done</span>' : ''}
777
- </div>`;
778
- }).join('');
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
- const len = document.getElementById('code-editor').value.length;
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 r = await fetch(`/tasks/${tid}`);
808
- const d = await r.json();
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 el = document.getElementById('alert-area');
824
- const cls = type === 'error' ? 'alert-error' : type === 'success' ? 'alert-success' : 'alert-info';
825
- el.innerHTML = `<div class="alert ${cls}">${escHtml(msg)}</div>`;
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 === 0) {
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).style.borderColor = '';
844
- document.getElementById('f-'+d).style.color = '';
845
  });
846
  document.getElementById('f-'+diff).style.borderColor = 'var(--accent)';
847
  document.getElementById('f-'+diff).style.color = 'var(--accent)';
848
-
849
- const tasks = diff === 'all' ? state.allTasks : state.allTasks.filter(t => t.difficulty === diff);
850
- const el = document.getElementById('task-list-container');
851
- if (!tasks.length) { el.innerHTML = '<div class="empty"><div class="empty-text">No tasks found</div></div>'; return; }
852
- el.innerHTML = tasks.map(t => `
853
- <div class="task-list-item" onclick="tryTask('${t.id}')">
854
- <div class="tli-header">
855
- <div class="tli-name">${t.id.replace(/_/g,' ')}</div>
856
- <span class="diff-tag ${t.difficulty}">${t.difficulty}</span>
857
- </div>
858
- <div class="tli-desc">${escHtml((t.description||'').slice(0,100))}${t.description?.length > 100 ? '…' : ''}</div>
859
- <div class="tli-footer">
860
- ${t.cwe_targets.map(c => `<span class="cwe">${c}</span>`).join('')}
861
- <span class="badge bo" style="font-size:9px;margin-left:auto">Try it β†’</span>
862
- </div>
863
- </div>
864
- `).join('');
865
  }
866
 
867
- function tryTask(taskId) {
868
- showPanel('playground', document.querySelector('.ntab'));
869
  document.querySelectorAll('.ntab')[0].click();
870
- document.getElementById('task-select').value = taskId;
871
  doReset();
872
  }
873
 
874
- // ── Weight chart ───────────────────────────────────────────────────────────
875
  function renderWeightChart() {
876
- const el = document.getElementById('weight-chart');
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">${name}</div>
885
- <div class="wbr-bg"><div class="wbr-fg" style="width:${w*100*3.33}%"></div></div>
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
- // ── Utils ──────────────────────────────────────────────────────────────────
897
- function escHtml(s) {
898
  return String(s||'').replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
899
  }
900
 
901
- // Tab key in textarea
902
  document.addEventListener('keydown', e => {
903
- if (e.target.id === 'code-editor' && e.key === 'Tab') {
904
  e.preventDefault();
905
- const s = e.target.selectionStart, en = e.target.selectionEnd;
906
- e.target.value = e.target.value.substring(0, s) + ' ' + e.target.value.substring(en);
907
- e.target.selectionStart = e.target.selectionEnd = s + 4;
908
  updateCharCount();
909
  }
910
- // Ctrl+Enter to submit
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>&nbsp;/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>&nbsp;/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>&nbsp;/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>&nbsp;/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>&nbsp;/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>&nbsp;/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>&nbsp;/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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;').replace(/"/g,'&quot;');
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>