Mahir commited on
Commit
c70f857
·
1 Parent(s): 40eba49

Complete dashboard rebuild — all 13 tasks, 4 domains

Browse files
Files changed (2) hide show
  1. README.md +60 -107
  2. static/index.html +551 -1004
README.md CHANGED
@@ -1,6 +1,6 @@
1
  ---
2
- title: Customer Support Triage OpenEnv
3
- emoji: 🎫
4
  colorFrom: indigo
5
  colorTo: purple
6
  sdk: docker
@@ -9,15 +9,15 @@ license: apache-2.0
9
  tags:
10
  - openenv
11
  - reinforcement-learning
12
- - customer-support
13
  - nlp
14
- - agent-environment
15
- short_description: OpenEnv environment for customer support triage agents
16
  ---
17
 
18
- # Customer Support Triage OpenEnv Environment
19
 
20
- > A real-world OpenEnv environment where AI agents learn to **triage, classify, prioritize, and resolve** customer support tickets across a SaaS platform.
21
 
22
  [![OpenEnv](https://img.shields.io/badge/OpenEnv-v1.0.0-6c63ff)](https://openenv.dev)
23
  [![HuggingFace Spaces](https://img.shields.io/badge/🤗%20Spaces-openenv--support-yellow)](https://huggingface.co/spaces)
@@ -27,86 +27,44 @@ short_description: OpenEnv environment for customer support triage agents
27
 
28
  ## 🎯 Environment Description
29
 
30
- Customer support triage is a high-stakes, real-world task that every software company faces. Agents must:
31
 
32
- - **Classify** incoming tickets accurately by category and urgency
33
- - **Draft** professional, factually grounded customer responses using a knowledge base
34
- - **Manage** a live queue of tickets across specialized agents under SLA constraints
35
-
36
- This environment models realistic support scenarios including billing disputes, API outages, SSO failures, account transfers, feature requests, and abuse reports — with varying customer tiers, sentiment signals, and time pressure.
37
 
38
  ---
39
 
40
- ## 📋 Tasks
41
-
42
- | Task | ID | Difficulty | Max Steps | Pass Threshold |
43
- |------|----|-----------|-----------|----------------|
44
- | Ticket Classification | `ticket_classification` | 🟢 Easy | 10 | 0.70 |
45
- | Response Drafting | `response_drafting` | 🟡 Medium | 6 | 0.60 |
46
- | SLA Queue Management | `queue_management` | 🔴 Hard | 40 | 0.50 |
47
-
48
- ### Task 1: Ticket Classification (Easy)
49
- Classify 10 incoming tickets by **category** (billing/technical/account/feature_request/abuse/unknown) and **priority** (P1-P4). Scores 0.6 for correct category, 0.4 for correct priority (with partial credit for adjacent priority levels).
50
-
51
- ### Task 2: Response Drafting (Medium)
52
- Draft complete customer-facing responses to pre-classified tickets. Graded on 5 dimensions using a deterministic rubric:
53
- - KB reference (25%) uses relevant knowledge base information
54
- - Issue addressed (30%) — directly addresses the customer's specific problem
55
- - Actionable steps (20%) provides concrete next steps
56
- - Tone & empathy (15%) professional, empathetic language
57
- - No hallucination (10%) no false promises or invented information
58
-
59
- ### Task 3: SLA Queue Management (Hard)
60
- Manage a queue of 20 mixed tickets across 3 specialized agents. Actions: `assign_ticket`, `escalate`, `resolve`, `close`, `no_op`. Score based on:
61
- - Resolution rate (35%)
62
- - SLA compliance (35%)
63
- - First-contact resolution rate (20%)
64
- - Escalation penalty (-20% if excessive)
65
-
66
- ---
67
-
68
- ## 🔌 Action & Observation Spaces
69
-
70
- ### Observation
71
- ```python
72
- class Observation(BaseModel):
73
- task_id: str
74
- step: int
75
- current_ticket: Optional[Ticket] # For tasks 1 & 2
76
- ticket_queue: List[Ticket] # For task 3
77
- agents: List[AgentInfo] # For task 3
78
- knowledge_base: List[KBArticle] # For task 2
79
- sla_status: Dict[str, str] # ticket_id -> ok/warning/breached
80
- valid_actions: List[str]
81
- episode_done: bool
82
- info: Dict[str, Any]
83
- ```
84
-
85
- ### Action
86
- ```python
87
- class Action(BaseModel):
88
- action_type: AgentAction # classify | draft_response | assign_ticket | escalate | resolve | close | no_op
89
- ticket_id: Optional[str]
90
- category: Optional[TicketCategory] # For classify
91
- priority: Optional[TicketPriority] # For classify
92
- response_text: Optional[str] # For draft_response
93
- target_agent_id: Optional[str] # For assign_ticket / escalate
94
- resolution_summary: Optional[str] # For resolve
95
- reasoning: Optional[str] # Optional transparency field
96
- ```
97
-
98
- ### Reward
99
- ```python
100
- class Reward(BaseModel):
101
- total: float # -1.0 to 1.0
102
- classification_accuracy: float
103
- response_quality: float
104
- sla_compliance: float
105
- first_contact_resolution: float
106
- customer_satisfaction: float
107
- penalty: float # Negative component
108
- breakdown: Dict[str, float] # Per-dimension scores
109
- ```
110
 
111
  ---
112
 
@@ -136,12 +94,10 @@ docker run -p 7860:7860 openenv-support
136
  ### Running the Baseline
137
 
138
  ```bash
139
- export OPENAI_API_KEY="sk-..."
140
  export OPENENV_BASE_URL="http://localhost:7860"
141
 
142
- python baseline_inference.py # All tasks
143
- python baseline_inference.py --task ticket_classification
144
- python baseline_inference.py --model gpt-4o
145
  ```
146
 
147
  ### OpenEnv Validate
@@ -152,17 +108,6 @@ openenv validate --url http://localhost:7860
152
 
153
  ---
154
 
155
- ## 📊 Baseline Scores
156
-
157
- | Agent | Classification | Drafting | Queue Mgmt | Overall |
158
- |-------|---------------|---------|------------|---------|
159
- | Heuristic (keyword-based) | 0.72 | 0.61 | 0.54 | 0.62 |
160
- | GPT-4o-mini | ~0.85 | ~0.74 | ~0.61 | ~0.73 |
161
-
162
- > Scores are deterministic given the same random seed. Run `/baseline` endpoint to reproduce.
163
-
164
- ---
165
-
166
  ## 🌐 API Reference
167
 
168
  | Endpoint | Method | Description |
@@ -188,16 +133,24 @@ openenv-support/
188
  ├── tasks/
189
  │ ├── task1_classification.py
190
  │ ├── task2_drafting.py
191
- ── task3_queue.py
 
 
 
 
192
  ├── graders/
193
- │ └── baseline_agent.py # Heuristic baseline
194
  ├── data/
195
- │ ├── tickets.py # Synthetic ticket dataset
196
- ── knowledge_base.py # KB articles
 
 
 
197
  ├── static/
198
- │ └── index.html # Interactive dashboard
199
  ├── openenv.yaml # OpenEnv metadata
200
- ├── baseline_inference.py # LLM baseline script
 
201
  ├── requirements.txt
202
  ├── Dockerfile
203
  └── README.md
@@ -207,8 +160,8 @@ openenv-support/
207
 
208
  ## 🏗️ Design Decisions
209
 
210
- **Why customer support?** It's a universal, high-value real-world task. Every SaaS company needs it. The task has natural difficulty progression (classification response quality queue optimization under constraints) and rich partial reward signals at every step.
211
 
212
- **Why deterministic graders?** The rubric uses keyword matching, heuristic NLP signals, and exact comparisons — no LLM-in-the-loop for grading, ensuring reproducible scores across runs.
213
 
214
- **Reward shaping:** Every step returns a non-zero reward signal. Even incorrect actions return small penalties rather than zero, giving RL agents a gradient to follow rather than a sparse reward landscape.
 
1
  ---
2
+ title: OpenEnvMulti-Domain
3
+ emoji: 🌐
4
  colorFrom: indigo
5
  colorTo: purple
6
  sdk: docker
 
9
  tags:
10
  - openenv
11
  - reinforcement-learning
12
+ - agents
13
  - nlp
14
+ - multi-domain
15
+ short_description: OpenEnv multi-domain evaluation environment for AI agents
16
  ---
17
 
18
+ # OpenEnv Multi-Domain AI Agent Environment
19
 
20
+ > A comprehensive OpenEnv environment where AI agents are evaluated across 13 complex real-world tasks spanning 4 professional domains: **Customer Support, Legal Review, Clinical Triage, and Software Engineering (PR Review)**.
21
 
22
  [![OpenEnv](https://img.shields.io/badge/OpenEnv-v1.0.0-6c63ff)](https://openenv.dev)
23
  [![HuggingFace Spaces](https://img.shields.io/badge/🤗%20Spaces-openenv--support-yellow)](https://huggingface.co/spaces)
 
27
 
28
  ## 🎯 Environment Description
29
 
30
+ To build general-purpose AI agents, we must evaluate them outside of traditional multiple-choice benchmarks. This environment provides 13 interactive, multi-step tasks across diverse verticals to test an agent's reasoning, tool use, and decision-making capabilities.
31
 
32
+ Agents must classify data, identify risks, draft domain-specific responses, and manage queues under dynamic constraints.
 
 
 
 
33
 
34
  ---
35
 
36
+ ## 📋 Tasks & Domains
37
+
38
+ This environment features 13 tasks across 4 distinct professional domains:
39
+
40
+ ### 🎫 Customer Support
41
+ | Task | ID | Difficulty | Max Steps |
42
+ |------|----|-----------|-----------|
43
+ | Ticket Classification | `ticket_classification` | 🟢 Easy | 10 |
44
+ | Response Drafting | `response_drafting` | 🟡 Medium | 6 |
45
+ | SLA Queue Management | `queue_management` | 🔴 Hard | 40 |
46
+ | Multi-Turn De-escalation | `multi_turn_conversation` | 🟣 Very Hard | 8 |
47
+
48
+ ### ⚖️ Legal Review
49
+ | Task | ID | Difficulty | Max Steps |
50
+ |------|----|-----------|-----------|
51
+ | Clause Identification | `legal_clause_identification` | 🟢 Easy | 10 |
52
+ | Risk Flagging | `legal_risk_flagging` | 🟡 Medium | 10 |
53
+ | Clause Redlining | `legal_clause_redlining` | 🔴 Hard | 5 |
54
+
55
+ ### 🏥 Clinical Triage
56
+ | Task | ID | Difficulty | Max Steps |
57
+ |------|----|-----------|-----------|
58
+ | Body System Classification | `clinical_triage_classification` | 🟢 Easy | 10 |
59
+ | ESI Level Assignment | `clinical_esi_assignment` | 🟡 Medium | 10 |
60
+ | Triage Note Generation | `clinical_triage_note` | 🔴 Hard | 5 |
61
+
62
+ ### 💻 PR Review (Software Engineering)
63
+ | Task | ID | Difficulty | Max Steps |
64
+ |------|----|-----------|-----------|
65
+ | PR Type Classification | `pr_type_classification` | 🟢 Easy | 10 |
66
+ | Bug Identification | `pr_bug_identification` | 🟡 Medium | 5 |
67
+ | Code Review Comment | `pr_review_comment` | 🔴 Hard | 5 |
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  ---
70
 
 
94
  ### Running the Baseline
95
 
96
  ```bash
 
97
  export OPENENV_BASE_URL="http://localhost:7860"
98
 
99
+ # Evaluate heuristic baseline on all tasks
100
+ python baseline_inference.py
 
101
  ```
102
 
103
  ### OpenEnv Validate
 
108
 
109
  ---
110
 
 
 
 
 
 
 
 
 
 
 
 
111
  ## 🌐 API Reference
112
 
113
  | Endpoint | Method | Description |
 
133
  ├── tasks/
134
  │ ├── task1_classification.py
135
  │ ├── task2_drafting.py
136
+ ── task3_queue.py
137
+ │ ├── task4_multiturn.py
138
+ │ ├── legal_tasks.py
139
+ │ ├── clinical_tasks.py
140
+ │ └── pr_tasks.py
141
  ├── graders/
142
+ │ └── baseline_agent.py # Auto-grader heuristics
143
  ├── data/
144
+ │ ├── tickets.py
145
+ ── knowledge_base.py
146
+ │ ├── legal_data.py
147
+ │ ├── clinical_data.py
148
+ │ └── pr_data.py
149
  ├── static/
150
+ │ └── index.html # Interactive multi-domain dashboard
151
  ├── openenv.yaml # OpenEnv metadata
152
+ ├── baseline_inference.py # Automated test runner for baseline
153
+ ├── validate.py # Validation script
154
  ├── requirements.txt
155
  ├── Dockerfile
156
  └── README.md
 
160
 
161
  ## 🏗️ Design Decisions
162
 
163
+ **Why multi-domain?** Real-world agents will need to operate across various specific verticals. Providing environments in Support, Legal, Clinical, and Engineering proves that an agent architecture is truly generalizable and robust.
164
 
165
+ **Why deterministic graders?** The rubric uses keyword matching, heuristic NLP signals (via \`sentence-transformers\`), semantic similarities, and exact comparisons — minimizing LLM-in-the-loop dependencies for grading and ensuring reproducible scores across runs for all 13 tasks.
166
 
167
+ **Dynamic Reward Shaping:** Every step returns a non-zero reward signal. Even incorrect actions return small penalties rather than zero, giving RL agents a gradient to follow rather than a sparse reward landscape.
static/index.html CHANGED
@@ -3,780 +3,294 @@
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
- <title>Customer Support Triage OpenEnv</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
9
  <style>
10
  :root {
11
- --bg: #0a0a0f;
12
- --surface: #111118;
13
- --surface2: #1a1a26;
14
- --border: #2a2a40;
15
  --accent: #6c63ff;
16
  --accent2: #ff6584;
17
  --accent3: #43e97b;
18
  --accent4: #f7971e;
 
19
  --text: #e8e8f0;
20
- --text-dim: #7878a0;
21
  --mono: 'Space Mono', monospace;
22
  --sans: 'Syne', sans-serif;
23
- --radius: 12px;
24
- --glow: 0 0 24px rgba(108,99,255,0.15);
25
  }
26
-
27
  * { box-sizing: border-box; margin: 0; padding: 0; }
28
-
29
- body {
30
- background: var(--bg);
31
- color: var(--text);
32
- font-family: var(--sans);
33
- min-height: 100vh;
34
- overflow-x: hidden;
35
- }
36
-
37
- /* Background grid */
38
  body::before {
39
- content: '';
40
- position: fixed;
41
- inset: 0;
42
- background-image:
43
- linear-gradient(rgba(108,99,255,0.04) 1px, transparent 1px),
44
- linear-gradient(90deg, rgba(108,99,255,0.04) 1px, transparent 1px);
45
- background-size: 40px 40px;
46
- pointer-events: none;
47
- z-index: 0;
48
  }
49
 
50
- .layout {
51
- position: relative;
52
- z-index: 1;
53
- display: grid;
54
- grid-template-rows: auto 1fr;
55
- min-height: 100vh;
56
- }
57
 
58
  /* ── Header ── */
59
  header {
60
- display: flex;
61
- align-items: center;
62
- justify-content: space-between;
63
- padding: 20px 32px;
64
- border-bottom: 1px solid var(--border);
65
- background: rgba(10,10,15,0.8);
66
- backdrop-filter: blur(12px);
67
- position: sticky;
68
- top: 0;
69
- z-index: 100;
70
  }
71
-
72
- .logo {
73
- display: flex;
74
- align-items: center;
75
- gap: 12px;
76
- }
77
-
78
  .logo-icon {
79
- width: 36px;
80
- height: 36px;
81
  background: linear-gradient(135deg, var(--accent), var(--accent2));
82
- border-radius: 8px;
83
- display: flex;
84
- align-items: center;
85
- justify-content: center;
86
- font-size: 18px;
87
  }
88
-
89
- .logo-text {
90
- font-size: 16px;
91
- font-weight: 700;
92
- letter-spacing: -0.3px;
 
 
 
93
  }
94
-
95
- .logo-sub {
96
- font-family: var(--mono);
97
- font-size: 11px;
98
- color: var(--text-dim);
99
- letter-spacing: 1px;
100
- margin-top: 1px;
101
- }
102
-
103
- .header-status {
104
- display: flex;
105
- align-items: center;
106
- gap: 8px;
107
- font-family: var(--mono);
108
- font-size: 12px;
109
- color: var(--accent3);
110
- }
111
-
112
- .pulse {
113
- width: 8px;
114
- height: 8px;
115
- background: var(--accent3);
116
- border-radius: 50%;
117
- animation: pulse 2s infinite;
118
- }
119
-
120
- @keyframes pulse {
121
- 0%, 100% { opacity: 1; transform: scale(1); }
122
- 50% { opacity: 0.5; transform: scale(0.8); }
123
- }
124
-
125
- /* ── Main content ── */
126
- .main {
127
- display: grid;
128
- grid-template-columns: 280px 1fr 320px;
129
- gap: 0;
130
- height: calc(100vh - 77px);
131
- overflow: hidden;
132
  }
133
 
134
  /* ── Sidebar ── */
135
- .sidebar {
136
- border-right: 1px solid var(--border);
137
- padding: 24px 20px;
138
- overflow-y: auto;
139
- display: flex;
140
- flex-direction: column;
141
- gap: 24px;
142
- }
143
-
144
- .sidebar-section-label {
145
- font-family: var(--mono);
146
- font-size: 10px;
147
- color: var(--text-dim);
148
- letter-spacing: 2px;
149
- text-transform: uppercase;
150
- margin-bottom: 12px;
151
  }
152
-
153
  .task-card {
154
- background: var(--surface);
155
- border: 1px solid var(--border);
156
- border-radius: var(--radius);
157
- padding: 16px;
158
- cursor: pointer;
159
- transition: all 0.2s;
160
- position: relative;
161
- overflow: hidden;
162
- }
163
-
164
- .task-card::before {
165
- content: '';
166
- position: absolute;
167
- left: 0;
168
- top: 0;
169
- bottom: 0;
170
- width: 3px;
171
- background: var(--accent);
172
- opacity: 0;
173
- transition: opacity 0.2s;
174
- }
175
-
176
- .task-card:hover, .task-card.active {
177
- border-color: var(--accent);
178
- background: var(--surface2);
179
- box-shadow: var(--glow);
180
  }
181
-
 
 
182
  .task-card.active::before { opacity: 1; }
183
-
184
- .task-name {
185
- font-weight: 700;
186
- font-size: 14px;
187
- margin-bottom: 4px;
188
- }
189
-
190
- .task-meta {
191
- display: flex;
192
- align-items: center;
193
- gap: 8px;
194
- margin-top: 8px;
195
- }
196
-
197
- .badge {
198
- font-family: var(--mono);
199
- font-size: 10px;
200
- padding: 2px 8px;
201
- border-radius: 4px;
202
- font-weight: 700;
203
- letter-spacing: 0.5px;
204
- }
205
-
206
- .badge-easy { background: rgba(67,233,123,0.15); color: var(--accent3); }
207
- .badge-medium { background: rgba(247,151,30,0.15); color: var(--accent4); }
208
- .badge-hard { background: rgba(255,101,132,0.15); color: var(--accent2); }
209
-
210
- .score-ring {
211
- font-family: var(--mono);
212
- font-size: 11px;
213
- color: var(--text-dim);
214
- }
215
-
216
- /* ── Center panel ── */
217
- .center {
218
- display: flex;
219
- flex-direction: column;
220
- overflow: hidden;
221
- }
222
-
223
  .center-header {
224
- padding: 20px 28px;
225
- border-bottom: 1px solid var(--border);
226
- display: flex;
227
- align-items: center;
228
- justify-content: space-between;
229
- flex-shrink: 0;
230
- }
231
-
232
- .center-title {
233
- font-size: 20px;
234
- font-weight: 800;
235
- }
236
-
237
- .action-bar {
238
- display: flex;
239
- gap: 10px;
240
- }
241
-
242
- .btn {
243
- font-family: var(--mono);
244
- font-size: 12px;
245
- padding: 8px 18px;
246
- border-radius: 8px;
247
- border: none;
248
- cursor: pointer;
249
- transition: all 0.15s;
250
- font-weight: 700;
251
- letter-spacing: 0.5px;
252
- }
253
-
254
- .btn-primary {
255
- background: var(--accent);
256
- color: #fff;
257
- }
258
-
259
  .btn-primary:hover { background: #7c74ff; transform: translateY(-1px); }
260
-
261
- .btn-secondary {
262
- background: var(--surface2);
263
- color: var(--text);
264
- border: 1px solid var(--border);
265
- }
266
-
267
  .btn-secondary:hover { border-color: var(--accent); }
268
-
269
- .btn-danger {
270
- background: rgba(255,101,132,0.15);
271
- color: var(--accent2);
272
- border: 1px solid rgba(255,101,132,0.3);
273
- }
274
-
275
- .btn-success {
276
- background: rgba(67,233,123,0.15);
277
- color: var(--accent3);
278
- border: 1px solid rgba(67,233,123,0.3);
279
- }
280
-
281
- .center-body {
282
- flex: 1;
283
- overflow-y: auto;
284
- padding: 24px 28px;
285
- }
286
-
287
- /* ── Ticket display ── */
288
- .ticket-card {
289
- background: var(--surface);
290
- border: 1px solid var(--border);
291
- border-radius: var(--radius);
292
- padding: 24px;
293
- margin-bottom: 16px;
294
- animation: slideIn 0.3s ease;
295
- }
296
-
297
- @keyframes slideIn {
298
- from { opacity: 0; transform: translateY(12px); }
299
- to { opacity: 1; transform: translateY(0); }
300
- }
301
-
302
- .ticket-header {
303
- display: flex;
304
- align-items: flex-start;
305
- justify-content: space-between;
306
- margin-bottom: 16px;
307
- gap: 16px;
308
- }
309
-
310
- .ticket-id {
311
- font-family: var(--mono);
312
- font-size: 11px;
313
- color: var(--accent);
314
- margin-bottom: 6px;
315
- }
316
-
317
- .ticket-subject {
318
- font-size: 18px;
319
- font-weight: 700;
320
- line-height: 1.3;
321
- }
322
-
323
- .ticket-tier {
324
- flex-shrink: 0;
325
- }
326
-
327
- .tier-badge {
328
- font-family: var(--mono);
329
- font-size: 10px;
330
- padding: 4px 10px;
331
- border-radius: 6px;
332
- font-weight: 700;
333
- }
334
-
335
- .tier-enterprise { background: rgba(108,99,255,0.2); color: var(--accent); border: 1px solid rgba(108,99,255,0.3); }
336
- .tier-pro { background: rgba(247,151,30,0.15); color: var(--accent4); border: 1px solid rgba(247,151,30,0.3); }
337
- .tier-free { background: rgba(120,120,160,0.15); color: var(--text-dim); border: 1px solid var(--border); }
338
-
339
- .ticket-body {
340
- font-size: 14px;
341
- line-height: 1.7;
342
- color: #c0c0d8;
343
- padding: 16px;
344
- background: var(--surface2);
345
- border-radius: 8px;
346
- border-left: 3px solid var(--border);
347
- margin-bottom: 16px;
348
- }
349
-
350
- .ticket-meta-row {
351
- display: flex;
352
- gap: 12px;
353
- flex-wrap: wrap;
354
- }
355
-
356
- .meta-chip {
357
- display: flex;
358
- align-items: center;
359
- gap: 6px;
360
- font-family: var(--mono);
361
- font-size: 11px;
362
- color: var(--text-dim);
363
- background: var(--surface2);
364
- padding: 4px 10px;
365
- border-radius: 6px;
366
- border: 1px solid var(--border);
367
- }
368
-
369
- .sentiment-bar {
370
- display: flex;
371
- align-items: center;
372
- gap: 8px;
373
- margin-top: 12px;
374
- }
375
-
376
- .sentiment-label {
377
- font-family: var(--mono);
378
- font-size: 10px;
379
- color: var(--text-dim);
380
- width: 80px;
381
- flex-shrink: 0;
382
- }
383
-
384
- .sentiment-track {
385
- flex: 1;
386
- height: 4px;
387
- background: var(--surface2);
388
- border-radius: 2px;
389
- overflow: hidden;
390
- }
391
-
392
- .sentiment-fill {
393
- height: 100%;
394
- border-radius: 2px;
395
- transition: width 0.5s ease;
396
- }
397
-
398
- /* ── KB articles ── */
399
- .kb-section { margin-top: 20px; }
400
-
401
- .kb-label {
402
- font-family: var(--mono);
403
- font-size: 10px;
404
- color: var(--text-dim);
405
- letter-spacing: 2px;
406
- text-transform: uppercase;
407
- margin-bottom: 10px;
408
- }
409
-
410
- .kb-article {
411
- background: var(--surface);
412
- border: 1px solid var(--border);
413
- border-radius: 8px;
414
- padding: 14px;
415
- margin-bottom: 8px;
416
- }
417
-
418
- .kb-article-title {
419
- font-size: 13px;
420
- font-weight: 700;
421
- margin-bottom: 6px;
422
- display: flex;
423
- align-items: center;
424
- gap: 8px;
425
- }
426
-
427
- .kb-article-id {
428
- font-family: var(--mono);
429
- font-size: 10px;
430
- color: var(--accent);
431
- }
432
-
433
- .kb-article-content {
434
- font-size: 12px;
435
- color: var(--text-dim);
436
- line-height: 1.6;
437
- }
438
 
439
  /* ── Action form ── */
440
- .action-form {
441
- background: var(--surface);
442
- border: 1px solid var(--border);
443
- border-radius: var(--radius);
444
- padding: 24px;
445
- margin-top: 16px;
446
- }
447
-
448
- .form-title {
449
- font-weight: 700;
450
- font-size: 15px;
451
- margin-bottom: 16px;
452
- display: flex;
453
- align-items: center;
454
- gap: 8px;
455
- }
456
-
457
- .form-row {
458
- display: grid;
459
- grid-template-columns: 1fr 1fr;
460
- gap: 12px;
461
- margin-bottom: 12px;
462
- }
463
-
464
- .form-group {
465
- display: flex;
466
- flex-direction: column;
467
- gap: 6px;
468
- }
469
-
470
- .form-label {
471
- font-family: var(--mono);
472
- font-size: 10px;
473
- color: var(--text-dim);
474
- letter-spacing: 1px;
475
- text-transform: uppercase;
476
- }
477
-
478
  .form-select, .form-textarea, .form-input {
479
- background: var(--surface2);
480
- border: 1px solid var(--border);
481
- border-radius: 8px;
482
- color: var(--text);
483
- font-family: var(--mono);
484
- font-size: 13px;
485
- padding: 10px 12px;
486
- transition: border-color 0.2s;
487
- width: 100%;
488
- }
489
-
490
- .form-select:focus, .form-textarea:focus, .form-input:focus {
491
- outline: none;
492
- border-color: var(--accent);
493
- }
494
-
495
- .form-textarea {
496
- font-family: var(--sans);
497
- font-size: 14px;
498
- resize: vertical;
499
- min-height: 120px;
500
- line-height: 1.6;
501
- }
502
-
503
- .submit-row {
504
- display: flex;
505
- justify-content: flex-end;
506
- margin-top: 16px;
507
  }
 
 
 
508
 
509
  /* ── Right panel ── */
510
- .right-panel {
511
- border-left: 1px solid var(--border);
512
- padding: 24px 20px;
513
- overflow-y: auto;
514
- display: flex;
515
- flex-direction: column;
516
- gap: 20px;
517
- }
518
-
519
- /* ── Score display ── */
520
- .score-card {
521
- background: var(--surface);
522
- border: 1px solid var(--border);
523
- border-radius: var(--radius);
524
- padding: 20px;
525
- }
526
-
527
- .score-card-title {
528
- font-family: var(--mono);
529
- font-size: 10px;
530
- color: var(--text-dim);
531
- letter-spacing: 2px;
532
- text-transform: uppercase;
533
- margin-bottom: 16px;
534
- }
535
-
536
- .big-score {
537
- font-family: var(--mono);
538
- font-size: 48px;
539
- font-weight: 700;
540
- line-height: 1;
541
- margin-bottom: 4px;
542
- }
543
-
544
- .score-label {
545
- font-family: var(--mono);
546
- font-size: 11px;
547
- color: var(--text-dim);
548
- }
549
-
550
- .breakdown-row {
551
- display: flex;
552
- justify-content: space-between;
553
- align-items: center;
554
- padding: 8px 0;
555
- border-bottom: 1px solid rgba(42,42,64,0.5);
556
- }
557
-
558
  .breakdown-row:last-child { border-bottom: none; }
559
-
560
- .breakdown-label {
561
- font-family: var(--mono);
562
- font-size: 11px;
563
- color: var(--text-dim);
564
- }
565
-
566
- .breakdown-val {
567
- font-family: var(--mono);
568
- font-size: 12px;
569
- font-weight: 700;
570
- }
571
-
572
- .val-good { color: var(--accent3); }
573
- .val-mid { color: var(--accent4); }
574
- .val-bad { color: var(--accent2); }
575
-
576
- /* ── Reward history chart ── */
577
- .mini-chart {
578
- height: 60px;
579
- display: flex;
580
- align-items: flex-end;
581
- gap: 3px;
582
- padding-top: 8px;
583
- }
584
-
585
- .bar {
586
- flex: 1;
587
- border-radius: 3px 3px 0 0;
588
- min-height: 4px;
589
- transition: height 0.3s ease;
590
- }
591
-
592
- /* ── Queue list ── */
593
- .queue-item {
594
- display: flex;
595
- align-items: center;
596
- gap: 10px;
597
- padding: 10px;
598
- background: var(--surface);
599
- border: 1px solid var(--border);
600
- border-radius: 8px;
601
- margin-bottom: 6px;
602
- font-size: 12px;
603
- transition: border-color 0.2s;
604
- }
605
-
606
- .queue-item:hover { border-color: var(--accent); }
607
-
608
- .priority-dot {
609
- width: 8px;
610
- height: 8px;
611
- border-radius: 50%;
612
- flex-shrink: 0;
613
- }
614
-
615
- .p1 { background: var(--accent2); box-shadow: 0 0 6px var(--accent2); }
616
- .p2 { background: var(--accent4); }
617
- .p3 { background: var(--accent3); }
618
- .p4 { background: var(--text-dim); }
619
-
620
- .queue-subject {
621
- flex: 1;
622
- overflow: hidden;
623
- text-overflow: ellipsis;
624
- white-space: nowrap;
625
- color: var(--text);
626
- }
627
-
628
- .queue-sla {
629
- font-family: var(--mono);
630
- font-size: 10px;
631
- color: var(--text-dim);
632
- flex-shrink: 0;
633
- }
634
-
635
- .sla-breached { color: var(--accent2); }
636
- .sla-warning { color: var(--accent4); }
637
- .sla-ok { color: var(--accent3); }
638
-
639
- /* ── Agent cards ── */
640
- .agent-card {
641
- background: var(--surface);
642
- border: 1px solid var(--border);
643
- border-radius: 8px;
644
- padding: 12px;
645
- margin-bottom: 8px;
646
- }
647
-
648
- .agent-name {
649
- font-size: 13px;
650
- font-weight: 700;
651
- margin-bottom: 8px;
652
- }
653
-
654
- .load-bar-track {
655
- height: 6px;
656
- background: var(--surface2);
657
- border-radius: 3px;
658
- overflow: hidden;
659
- margin-top: 6px;
660
- }
661
-
662
- .load-bar-fill {
663
- height: 100%;
664
- border-radius: 3px;
665
- background: linear-gradient(90deg, var(--accent3), var(--accent));
666
- transition: width 0.4s ease;
667
- }
668
-
669
- .load-bar-fill.overloaded { background: linear-gradient(90deg, var(--accent4), var(--accent2)); }
670
-
671
- /* ── Log ── */
672
- .log-entry {
673
- font-family: var(--mono);
674
- font-size: 11px;
675
- color: var(--text-dim);
676
- padding: 6px 10px;
677
- border-radius: 4px;
678
- margin-bottom: 4px;
679
- background: var(--surface);
680
- }
681
-
682
- .log-reward-pos { color: var(--accent3); }
683
- .log-reward-neg { color: var(--accent2); }
684
-
685
- /* ── Empty state ── */
686
- .empty-state {
687
- display: flex;
688
- flex-direction: column;
689
- align-items: center;
690
- justify-content: center;
691
- height: 300px;
692
- color: var(--text-dim);
693
- text-align: center;
694
- gap: 12px;
695
- }
696
-
697
- .empty-icon { font-size: 48px; opacity: 0.3; }
698
- .empty-text { font-size: 14px; }
699
 
700
  /* ── Episode done ── */
701
- .episode-done {
702
- background: linear-gradient(135deg, var(--surface2), rgba(108,99,255,0.08));
703
- border: 1px solid rgba(108,99,255,0.3);
704
- border-radius: var(--radius);
705
- padding: 32px;
706
- text-align: center;
707
- animation: slideIn 0.4s ease;
708
- }
709
-
710
- .done-icon { font-size: 48px; margin-bottom: 16px; }
711
- .done-title { font-size: 24px; font-weight: 800; margin-bottom: 8px; }
712
- .done-score { font-family: var(--mono); font-size: 64px; font-weight: 700; color: var(--accent); }
713
 
714
- /* Scrollbar */
715
- ::-webkit-scrollbar { width: 4px; }
716
- ::-webkit-scrollbar-track { background: transparent; }
717
- ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
 
 
 
 
 
 
 
 
 
 
718
  </style>
719
  </head>
720
  <body>
721
  <div class="layout">
722
  <header>
723
  <div class="logo">
724
- <div class="logo-icon">🎫</div>
725
  <div>
726
- <div class="logo-text">Support Triage</div>
727
- <div class="logo-sub">OPENENV ENVIRONMENT</div>
728
  </div>
729
  </div>
730
- <div class="header-status">
731
- <div class="pulse"></div>
732
- <span id="server-status">LIVE</span>
733
  </div>
734
  </header>
735
 
736
  <div class="main">
737
  <!-- Sidebar -->
738
  <aside class="sidebar">
739
- <div>
740
- <div class="sidebar-section-label">Tasks</div>
741
- <div class="task-card active" id="task-classification" onclick="selectTask('ticket_classification')">
 
742
  <div class="task-name">Ticket Classification</div>
743
- <div style="font-size:12px;color:var(--text-dim);margin-top:4px;">Classify tickets by category & priority</div>
744
- <div class="task-meta">
745
- <span class="badge badge-easy">EASY</span>
746
- <span class="score-ring" id="score-classification">—</span>
747
- </div>
748
  </div>
749
- <div class="task-card" id="task-drafting" onclick="selectTask('response_drafting')" style="margin-top:8px;">
750
  <div class="task-name">Response Drafting</div>
751
- <div style="font-size:12px;color:var(--text-dim);margin-top:4px;">Write customer-facing responses</div>
752
- <div class="task-meta">
753
- <span class="badge badge-medium">MEDIUM</span>
754
- <span class="score-ring" id="score-drafting">—</span>
755
- </div>
756
  </div>
757
- <div class="task-card" id="task-queue" onclick="selectTask('queue_management')" style="margin-top:8px;">
758
- <div class="task-name">Queue Management</div>
759
- <div style="font-size:12px;color:var(--text-dim);margin-top:4px;">Manage 20 tickets across 3 agents</div>
760
- <div class="task-meta">
761
- <span class="badge badge-hard">HARD</span>
762
- <span class="score-ring" id="score-queue">—</span>
763
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
764
  </div>
765
  </div>
766
 
767
  <div>
768
- <div class="sidebar-section-label">Episode Log</div>
769
- <div id="log-container" style="max-height:250px;overflow-y:auto;"></div>
770
  </div>
771
 
772
  <div>
773
- <div class="sidebar-section-label">Links</div>
774
- <a href="/docs" target="_blank" style="display:flex;align-items:center;gap:8px;color:var(--accent);font-size:13px;text-decoration:none;padding:8px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">
775
- <span>📖</span> API Docs (Swagger)
776
- </a>
777
- <a href="/tasks" target="_blank" style="display:flex;align-items:center;gap:8px;color:var(--text-dim);font-size:13px;text-decoration:none;padding:8px;background:var(--surface);border-radius:8px;border:1px solid var(--border);margin-top:6px;">
778
- <span>📋</span> /tasks endpoint
779
- </a>
780
  </div>
781
  </aside>
782
 
@@ -785,20 +299,18 @@
785
  <div class="center-header">
786
  <div>
787
  <div class="center-title" id="center-title">Ticket Classification</div>
788
- <div style="font-family:var(--mono);font-size:12px;color:var(--text-dim);margin-top:4px;" id="center-subtitle">
789
- Step <span id="step-counter">0</span> / <span id="max-steps">10</span>
790
- </div>
791
  </div>
792
  <div class="action-bar">
793
- <button class="btn btn-secondary" onclick="runBaseline()">▶ Heuristic Baseline</button>
794
  <button class="btn btn-primary" onclick="resetEpisode()">↺ Reset Episode</button>
795
  </div>
796
  </div>
797
-
798
  <div class="center-body" id="center-body">
799
  <div class="empty-state">
800
- <div class="empty-icon">🎫</div>
801
- <div class="empty-text">Click "Reset Episode" to start a new episode</div>
 
802
  </div>
803
  </div>
804
  </main>
@@ -806,33 +318,28 @@
806
  <!-- Right panel -->
807
  <aside class="right-panel">
808
  <div>
809
- <div class="sidebar-section-label">Current Score</div>
810
  <div class="score-card">
811
- <div class="score-card-title">EPISODE SCORE</div>
812
  <div class="big-score" id="current-score" style="color:var(--accent)">—</div>
813
  <div class="score-label" id="score-status">No active episode</div>
814
-
815
- <div style="margin-top:16px;" id="score-breakdown"></div>
816
  </div>
817
  </div>
818
-
819
  <div>
820
- <div class="sidebar-section-label">Reward History</div>
821
  <div class="score-card">
822
  <div class="mini-chart" id="reward-chart"></div>
823
- <div style="display:flex;justify-content:space-between;margin-top:6px;font-family:var(--mono);font-size:10px;color:var(--text-dim);">
824
  <span>+1.0</span><span>avg: <span id="avg-reward">—</span></span><span>-1.0</span>
825
  </div>
826
  </div>
827
  </div>
828
-
829
  <div id="agents-section" style="display:none;">
830
- <div class="sidebar-section-label">Agent Status</div>
831
  <div id="agents-container"></div>
832
  </div>
833
-
834
  <div id="queue-section" style="display:none;">
835
- <div class="sidebar-section-label">Ticket Queue</div>
836
  <div id="queue-container"></div>
837
  </div>
838
  </aside>
@@ -844,12 +351,21 @@ const API = '';
844
  let currentTaskId = 'ticket_classification';
845
  let currentObs = null;
846
  let rewardHistory = [];
847
- let episodeScore = null;
848
 
849
  const TASK_CONFIG = {
850
- ticket_classification: { title: 'Ticket Classification', maxSteps: 10 },
851
- response_drafting: { title: 'Response Drafting', maxSteps: 6 },
852
- queue_management: { title: 'Queue Management', maxSteps: 40 },
 
 
 
 
 
 
 
 
 
 
853
  };
854
 
855
  async function api(method, path, body) {
@@ -863,18 +379,16 @@ async function api(method, path, body) {
863
  function selectTask(taskId) {
864
  currentTaskId = taskId;
865
  document.querySelectorAll('.task-card').forEach(c => c.classList.remove('active'));
866
- document.getElementById('task-' + taskId.split('_')[0]).classList.add('active');
867
- // Handle 'classification' vs 'ticket_classification'
868
- const map = { ticket_classification: 'classification', response_drafting: 'drafting', queue_management: 'queue' };
869
- document.getElementById('task-' + map[taskId]).classList.add('active');
870
-
871
  const cfg = TASK_CONFIG[taskId];
 
872
  document.getElementById('center-title').textContent = cfg.title;
 
873
  document.getElementById('max-steps').textContent = cfg.maxSteps;
 
874
  document.getElementById('center-body').innerHTML = `
875
  <div class="empty-state">
876
- <div class="empty-icon">🎫</div>
877
- <div class="empty-text">Click "Reset Episode" to start with this task</div>
878
  </div>`;
879
  rewardHistory = [];
880
  updateRewardChart();
@@ -885,243 +399,297 @@ function selectTask(taskId) {
885
  document.getElementById('queue-section').style.display = 'none';
886
  }
887
 
888
- // Fix task card click handlers
889
- document.getElementById('task-classification').onclick = () => selectTask('ticket_classification');
890
- document.getElementById('task-drafting').onclick = () => selectTask('response_drafting');
891
- document.getElementById('task-queue').onclick = () => selectTask('queue_management');
892
-
893
  async function resetEpisode() {
894
  rewardHistory = [];
895
- episodeScore = null;
896
  document.getElementById('log-container').innerHTML = '';
897
-
898
  try {
899
  const data = await api('POST', `/reset?task_id=${currentTaskId}`);
900
  currentObs = data.observation;
901
  document.getElementById('step-counter').textContent = currentObs.step || 0;
902
  renderObservation(currentObs);
903
- addLog(` Episode reset task: ${currentTaskId}`, 'info');
904
  updateAgents(currentObs.agents);
905
  updateQueue(currentObs.ticket_queue, currentObs.sla_status);
906
- } catch(e) {
907
- addLog(`❌ Reset failed: ${e.message}`, 'error');
908
- }
909
  }
910
 
911
  function renderObservation(obs) {
912
- if (obs.episode_done) {
913
- fetchAndShowGrader();
914
- return;
915
- }
916
-
917
  const body = document.getElementById('center-body');
 
 
 
 
918
 
919
- if (currentTaskId === 'ticket_classification') {
920
- renderClassificationView(obs, body);
921
- } else if (currentTaskId === 'response_drafting') {
922
- renderDraftingView(obs, body);
923
- } else {
924
- renderQueueView(obs, body);
925
- }
926
 
927
  updateAgents(obs.agents);
928
  updateQueue(obs.ticket_queue, obs.sla_status);
929
  }
930
 
931
- function renderTicketHTML(ticket) {
932
- if (!ticket) return '';
933
- const sentiment = ticket.sentiment_score || 0;
934
- const sentPct = ((sentiment + 1) / 2 * 100).toFixed(0);
935
- const sentColor = sentiment < -0.3 ? 'var(--accent2)' : sentiment > 0.3 ? 'var(--accent3)' : 'var(--accent4)';
 
 
 
 
 
 
936
 
937
- return `
938
  <div class="ticket-card">
939
- <div class="ticket-header">
940
- <div>
941
- <div class="ticket-id">${ticket.ticket_id}</div>
942
- <div class="ticket-subject">${ticket.subject}</div>
943
- </div>
944
- <div class="ticket-tier">
945
- <span class="tier-badge tier-${ticket.customer_tier}">${ticket.customer_tier.toUpperCase()}</span>
946
- </div>
947
- </div>
948
- <div class="ticket-body">${ticket.body}</div>
949
- <div class="ticket-meta-row">
950
- ${ticket.category ? `<span class="meta-chip">📁 ${ticket.category}</span>` : ''}
951
- ${ticket.priority ? `<span class="meta-chip">🔺 ${ticket.priority}</span>` : ''}
952
- ${ticket.status ? `<span class="meta-chip">📌 ${ticket.status}</span>` : ''}
953
- <span class="meta-chip">🕐 SLA: ${ticket.sla_deadline ? new Date(ticket.sla_deadline).toLocaleTimeString() : '—'}</span>
954
  </div>
955
- <div class="sentiment-bar">
956
- <span class="sentiment-label">SENTIMENT</span>
957
- <div class="sentiment-track">
958
- <div class="sentiment-fill" style="width:${sentPct}%;background:${sentColor}"></div>
959
- </div>
960
- <span style="font-family:var(--mono);font-size:11px;color:var(--text-dim);width:40px;text-align:right">${sentiment.toFixed(2)}</span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
961
  </div>
962
- </div>`;
 
 
 
963
  }
964
 
965
- function renderKBHTML(articles) {
966
- if (!articles || !articles.length) return '';
967
- return `
968
- <div class="kb-section">
969
- <div class="kb-label">Knowledge Base</div>
970
- ${articles.map(a => `
971
- <div class="kb-article">
972
- <div class="kb-article-title">
973
- <span class="kb-article-id">${a.article_id}</span>
974
- ${a.title}
975
- </div>
976
- <div class="kb-article-content">${a.content}</div>
977
- </div>`).join('')}
978
- </div>`;
 
 
 
979
  }
980
 
981
- function renderClassificationView(obs, body) {
982
- const ticket = obs.current_ticket;
983
- body.innerHTML = renderTicketHTML(ticket) + `
984
- <div class="action-form">
985
- <div class="form-title"> Classify Ticket</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
986
  <div class="form-row">
987
- <div class="form-group">
988
- <label class="form-label">Category</label>
989
  <select class="form-select" id="f-category">
990
- <option value="">-- Select --</option>
991
- ${['billing','technical','account','feature_request','abuse','unknown'].map(c =>
992
- `<option value="${c}">${c.replace('_',' ').toUpperCase()}</option>`
993
- ).join('')}
994
- </select>
995
- </div>
996
- <div class="form-group">
997
- <label class="form-label">Priority</label>
998
  <select class="form-select" id="f-priority">
999
- <option value="">-- Select --</option>
1000
- ${['P1','P2','P3','P4'].map(p =>
1001
- `<option value="${p}">${p} — ${{'P1':'Critical','P2':'High','P3':'Medium','P4':'Low'}[p]}</option>`
1002
- ).join('')}
1003
- </select>
1004
- </div>
1005
- </div>
1006
- <div class="form-group" style="margin-bottom:12px;">
1007
- <label class="form-label">Reasoning (optional)</label>
1008
- <input class="form-input" id="f-reasoning" placeholder="Why did you choose this category/priority?"/>
1009
- </div>
1010
- <div class="submit-row">
1011
- <button class="btn btn-primary" onclick="submitClassify('${ticket?.ticket_id}')">Submit Classification →</button>
1012
  </div>
 
1013
  </div>`;
1014
- }
1015
 
1016
- function renderDraftingView(obs, body) {
1017
- const ticket = obs.current_ticket;
1018
- body.innerHTML = renderTicketHTML(ticket) + renderKBHTML(obs.knowledge_base) + `
1019
- <div class="action-form">
1020
- <div class="form-title">✍️ Draft Response</div>
1021
- <div class="form-group" style="margin-bottom:12px;">
1022
- <label class="form-label">Customer Response</label>
1023
- <textarea class="form-textarea" id="f-response" rows="8" style="min-height:200px;"
1024
- placeholder="Write a professional, empathetic response that addresses the customer's issue with concrete next steps..."></textarea>
1025
- </div>
1026
- <div class="submit-row">
1027
- <button class="btn btn-primary" onclick="submitDraft('${ticket?.ticket_id}')">Send Response →</button>
1028
- </div>
1029
  </div>`;
1030
- }
1031
-
1032
- function renderQueueView(obs, body) {
1033
- const queue = obs.ticket_queue || [];
1034
- const info = obs.info || {};
1035
 
1036
- body.innerHTML = `
1037
- <div style="display:grid;grid-template-columns:1fr 1fr 1fr;gap:12px;margin-bottom:20px;">
1038
- <div class="score-card" style="text-align:center;">
1039
- <div class="score-card-title">OPEN</div>
1040
- <div style="font-family:var(--mono);font-size:32px;font-weight:700;color:var(--accent)">${info.open_count ?? queue.length}</div>
1041
- </div>
1042
- <div class="score-card" style="text-align:center;">
1043
- <div class="score-card-title">RESOLVED</div>
1044
- <div style="font-family:var(--mono);font-size:32px;font-weight:700;color:var(--accent3)">${info.resolved ?? 0}</div>
1045
- </div>
1046
- <div class="score-card" style="text-align:center;">
1047
- <div class="score-card-title">SLA BREACHES</div>
1048
- <div style="font-family:var(--mono);font-size:32px;font-weight:700;color:var(--accent2)">${info.sla_breaches ?? 0}</div>
1049
- </div>
1050
- </div>
1051
- <div class="action-form">
1052
- <div class="form-title">⚙️ Queue Action</div>
1053
  <div class="form-row">
1054
- <div class="form-group">
1055
- <label class="form-label">Ticket ID</label>
1056
  <select class="form-select" id="f-ticket-id">
1057
- <option value="">-- Select Ticket --</option>
1058
- ${queue.map(t => `<option value="${t.ticket_id}">${t.ticket_id} | ${t.priority || '?'} | ${t.subject.substring(0,40)}</option>`).join('')}
1059
- </select>
1060
- </div>
1061
- <div class="form-group">
1062
- <label class="form-label">Action Type</label>
1063
- <select class="form-select" id="f-action-type" onchange="toggleQueueFields()">
1064
- ${['assign_ticket','escalate','resolve','close','no_op'].map(a =>
1065
- `<option value="${a}">${a.replace('_',' ').toUpperCase()}</option>`
1066
- ).join('')}
1067
- </select>
1068
- </div>
1069
  </div>
1070
- <div id="agent-row" class="form-group" style="margin-bottom:12px;">
1071
- <label class="form-label">Assign To Agent</label>
1072
  <select class="form-select" id="f-agent-id">
1073
- <option value="agent_billing">agent_billing — Jordan (Billing)</option>
1074
- <option value="agent_tech">agent_tech — Sam (Technical)</option>
1075
- <option value="agent_general">agent_general — Alex (General)</option>
1076
- </select>
 
 
 
 
 
 
 
 
 
 
 
 
 
1077
  </div>
1078
- <div class="submit-row">
1079
- <button class="btn btn-primary" onclick="submitQueueAction()">Execute Action →</button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1080
  </div>
 
1081
  </div>`;
1082
- }
1083
 
1084
- function toggleQueueFields() {
1085
- const action = document.getElementById('f-action-type')?.value;
1086
- const agentRow = document.getElementById('agent-row');
1087
- if (agentRow) {
1088
- agentRow.style.display = ['assign_ticket', 'escalate'].includes(action) ? 'flex' : 'none';
 
1089
  }
1090
- }
1091
 
1092
- async function submitClassify(ticketId) {
1093
- const category = document.getElementById('f-category').value;
1094
- const priority = document.getElementById('f-priority').value;
1095
- const reasoning = document.getElementById('f-reasoning').value;
 
 
 
 
 
1096
 
1097
- if (!category || !priority) { alert('Please select both category and priority.'); return; }
 
 
 
 
 
 
 
 
 
 
 
 
1098
 
1099
- await submitAction({
1100
- action_type: 'classify',
1101
- ticket_id: ticketId,
1102
- category,
1103
- priority,
1104
- reasoning,
1105
- });
1106
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1107
 
1108
- async function submitDraft(ticketId) {
1109
- const response_text = document.getElementById('f-response').value;
1110
- if (!response_text.trim()) { alert('Please write a response.'); return; }
 
 
 
 
1111
 
1112
- await submitAction({
1113
- action_type: 'draft_response',
1114
- ticket_id: ticketId,
1115
- response_text,
1116
- });
1117
  }
1118
 
1119
- async function submitQueueAction() {
 
 
 
 
 
 
1120
  const action_type = document.getElementById('f-action-type').value;
1121
  const ticket_id = document.getElementById('f-ticket-id')?.value;
1122
  const target_agent_id = document.getElementById('f-agent-id')?.value;
1123
-
1124
- await submitAction({ action_type, ticket_id, target_agent_id });
1125
  }
1126
 
1127
  async function submitAction(actionBody) {
@@ -1129,63 +697,48 @@ async function submitAction(actionBody) {
1129
  const data = await api('POST', `/step?task_id=${currentTaskId}`, actionBody);
1130
  currentObs = data.observation;
1131
  const reward = data.reward;
1132
-
1133
  rewardHistory.push(reward.total);
1134
  document.getElementById('step-counter').textContent = currentObs.step || rewardHistory.length;
1135
-
1136
  const sign = reward.total >= 0 ? '+' : '';
1137
- addLog(`Step ${rewardHistory.length}: ${sign}${reward.total.toFixed(3)} reward`, reward.total >= 0 ? 'pos' : 'neg');
1138
-
1139
  updateRewardChart();
1140
  updateScoreDisplay(reward);
1141
  renderObservation(currentObs);
1142
-
1143
- if (data.done) {
1144
- fetchAndShowGrader();
1145
- }
1146
- } catch(e) {
1147
- addLog(`❌ Action failed: ${e.message}`, 'error');
1148
- }
1149
  }
1150
 
1151
- async function fetchAndShowGrader() {
1152
  try {
1153
  const result = await api('POST', `/grader?task_id=${currentTaskId}`);
1154
- showEpisodeDone(result);
1155
- const scoreKey = { ticket_classification: 'classification', response_drafting: 'drafting', queue_management: 'queue' }[currentTaskId];
1156
- document.getElementById(`score-${scoreKey}`).textContent = result.final_score.toFixed(3);
1157
- } catch(e) {
1158
- console.error(e);
1159
- }
1160
  }
1161
 
1162
- function showEpisodeDone(result) {
1163
- const passed = result.passed;
1164
  const score = result.final_score;
1165
  const color = score >= 0.7 ? 'var(--accent3)' : score >= 0.4 ? 'var(--accent4)' : 'var(--accent2)';
1166
  const emoji = score >= 0.7 ? '🏆' : score >= 0.4 ? '📊' : '📉';
1167
-
1168
  document.getElementById('center-body').innerHTML = `
1169
  <div class="episode-done">
1170
- <div class="done-icon">${emoji}</div>
1171
  <div class="done-title">Episode Complete</div>
1172
  <div class="done-score" style="color:${color}">${score.toFixed(4)}</div>
1173
- <div style="font-family:var(--mono);font-size:14px;color:var(--text-dim);margin:8px 0 24px">
1174
- ${passed ? '✓ PASSED' : '✗ DID NOT PASS'} &nbsp;|&nbsp; ${rewardHistory.length} steps
1175
  </div>
1176
- <div style="text-align:left;max-width:400px;margin:0 auto;">
1177
- ${Object.entries(result.metrics || {}).filter(([k]) => typeof result.metrics[k] === 'number').map(([k,v]) => `
1178
- <div class="breakdown-row">
1179
- <span class="breakdown-label">${k.replace(/_/g,' ')}</span>
1180
- <span class="breakdown-val ${v >= 0.7 ? 'val-good' : v >= 0.4 ? 'val-mid' : 'val-bad'}">${typeof v === 'number' ? v.toFixed(4) : v}</span>
1181
- </div>`).join('')}
1182
- </div>
1183
- <button class="btn btn-primary" style="margin-top:24px;" onclick="resetEpisode()">↺ New Episode</button>
1184
  </div>`;
1185
-
1186
  document.getElementById('current-score').textContent = score.toFixed(4);
1187
  document.getElementById('current-score').style.color = color;
1188
- document.getElementById('score-status').textContent = passed ? '✓ Passed' : '✗ Did not pass';
1189
  }
1190
 
1191
  function updateScoreDisplay(reward) {
@@ -1193,107 +746,101 @@ function updateScoreDisplay(reward) {
1193
  const color = total >= 0.6 ? 'var(--accent3)' : total >= 0.2 ? 'var(--accent4)' : 'var(--accent2)';
1194
  document.getElementById('current-score').textContent = total.toFixed(4);
1195
  document.getElementById('current-score').style.color = color;
1196
- document.getElementById('score-status').textContent = `Step reward`;
1197
-
1198
  const bd = reward.breakdown || {};
1199
  document.getElementById('score-breakdown').innerHTML = Object.entries(bd).map(([k,v]) => `
1200
  <div class="breakdown-row">
1201
  <span class="breakdown-label">${k.replace(/_/g,' ')}</span>
1202
- <span class="breakdown-val ${v >= 0.6 ? 'val-good' : v < 0 ? 'val-bad' : 'val-mid'}">${typeof v === 'number' ? v.toFixed(3) : v}</span>
1203
  </div>`).join('');
1204
  }
1205
 
1206
  function updateRewardChart() {
1207
  const container = document.getElementById('reward-chart');
1208
- if (!rewardHistory.length) { container.innerHTML = ''; return; }
1209
-
1210
- const avg = (rewardHistory.reduce((a,b) => a+b, 0) / rewardHistory.length).toFixed(3);
1211
  document.getElementById('avg-reward').textContent = avg;
1212
-
1213
  container.innerHTML = rewardHistory.map(r => {
1214
- const pct = ((r + 1) / 2 * 100).toFixed(1);
1215
- const color = r >= 0.3 ? 'var(--accent3)' : r >= 0 ? 'var(--accent4)' : 'var(--accent2)';
1216
  return `<div class="bar" style="height:${pct}%;background:${color};opacity:0.8;"></div>`;
1217
  }).join('');
1218
  }
1219
 
1220
  function updateAgents(agents) {
1221
- if (!agents || !agents.length) {
1222
- document.getElementById('agents-section').style.display = 'none';
1223
- return;
1224
- }
1225
- document.getElementById('agents-section').style.display = 'block';
1226
  document.getElementById('agents-container').innerHTML = agents.map(a => {
1227
- const pct = (a.current_load / a.max_load * 100).toFixed(0);
1228
- const overloaded = a.current_load >= a.max_load;
1229
- return `
1230
- <div class="agent-card">
1231
- <div class="agent-name">${a.name}</div>
1232
- <div style="font-family:var(--mono);font-size:10px;color:var(--text-dim);">
1233
- Load: ${a.current_load}/${a.max_load} &nbsp;|&nbsp; ${a.specialization.join(', ')}
1234
- </div>
1235
- <div class="load-bar-track">
1236
- <div class="load-bar-fill ${overloaded ? 'overloaded' : ''}" style="width:${pct}%"></div>
1237
- </div>
1238
- </div>`;
1239
  }).join('');
1240
  }
1241
 
1242
  function updateQueue(queue, slaStatus) {
1243
- if (!queue || !queue.length) {
1244
- document.getElementById('queue-section').style.display = 'none';
1245
- return;
1246
- }
1247
- document.getElementById('queue-section').style.display = 'block';
1248
  document.getElementById('queue-container').innerHTML = queue.slice(0,8).map(t => {
1249
- const sla = (slaStatus || {})[t.ticket_id] || 'ok';
1250
- const p = (t.priority || 'P3').toLowerCase().replace('_critical','').replace('_high','').replace('_medium','').replace('_low','');
1251
- return `
1252
- <div class="queue-item">
1253
- <div class="priority-dot ${p}"></div>
1254
- <span class="queue-subject">${t.subject}</span>
1255
- <span class="queue-sla sla-${sla}">${sla.toUpperCase()}</span>
1256
- </div>`;
1257
  }).join('');
1258
  }
1259
 
1260
  function addLog(message, type) {
1261
  const container = document.getElementById('log-container');
1262
  const div = document.createElement('div');
1263
- div.className = `log-entry ${type === 'pos' ? 'log-reward-pos' : type === 'neg' ? 'log-reward-neg' : ''}`;
1264
  div.textContent = message;
1265
  container.insertBefore(div, container.firstChild);
1266
- if (container.children.length > 30) container.removeChild(container.lastChild);
1267
  }
1268
 
1269
  async function runBaseline() {
1270
- addLog('⚡ Running heuristic baseline...', 'info');
1271
  try {
1272
  const data = await api('POST', '/baseline');
1273
- const tasks = data.tasks;
1274
- Object.entries(tasks).forEach(([tid, result]) => {
1275
- const map = { ticket_classification: 'classification', response_drafting: 'drafting', queue_management: 'queue' };
1276
- if (map[tid]) {
1277
- document.getElementById(`score-${map[tid]}`).textContent = result.final_score.toFixed(3);
1278
- }
1279
- addLog(`${tid}: ${result.final_score.toFixed(4)} ${result.passed ? '✓' : '✗'}`, result.final_score > 0.5 ? 'pos' : 'neg');
1280
  });
1281
- addLog(`Overall: ${data.overall_score.toFixed(4)}`, data.overall_score > 0.5 ? 'pos' : 'neg');
1282
- } catch(e) {
1283
- addLog(`Baseline error: ${e.message}`, 'neg');
 
 
 
 
 
 
 
 
 
 
1284
  }
1285
  }
1286
 
1287
- // Check server health on load
1288
  (async () => {
1289
  try {
1290
- await api('GET', '/health');
1291
  document.getElementById('server-status').textContent = 'LIVE';
 
1292
  } catch(e) {
1293
  document.getElementById('server-status').textContent = 'OFFLINE';
1294
  document.getElementById('server-status').style.color = 'var(--accent2)';
 
 
 
1295
  }
1296
  })();
1297
  </script>
1298
  </body>
1299
- </html>
 
3
  <head>
4
  <meta charset="UTF-8"/>
5
  <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
6
+ <title>OpenEnv Multi-Domain AI Agent Environment</title>
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
  <link href="https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&family=Syne:wght@400;600;700;800&display=swap" rel="stylesheet">
9
  <style>
10
  :root {
11
+ --bg: #07070f;
12
+ --surface: #0f0f1a;
13
+ --surface2: #181826;
14
+ --border: #252538;
15
  --accent: #6c63ff;
16
  --accent2: #ff6584;
17
  --accent3: #43e97b;
18
  --accent4: #f7971e;
19
+ --accent5: #00d2ff;
20
  --text: #e8e8f0;
21
+ --text-dim: #6868a0;
22
  --mono: 'Space Mono', monospace;
23
  --sans: 'Syne', sans-serif;
24
+ --radius: 10px;
 
25
  }
 
26
  * { box-sizing: border-box; margin: 0; padding: 0; }
27
+ body { background: var(--bg); color: var(--text); font-family: var(--sans); min-height: 100vh; overflow-x: hidden; }
 
 
 
 
 
 
 
 
 
28
  body::before {
29
+ content: ''; position: fixed; inset: 0;
30
+ background-image: linear-gradient(rgba(108,99,255,0.03) 1px, transparent 1px), linear-gradient(90deg, rgba(108,99,255,0.03) 1px, transparent 1px);
31
+ background-size: 40px 40px; pointer-events: none; z-index: 0;
 
 
 
 
 
 
32
  }
33
 
34
+ /* ── Layout ── */
35
+ .layout { position: relative; z-index: 1; display: grid; grid-template-rows: auto 1fr; min-height: 100vh; }
36
+ .main { display: grid; grid-template-columns: 260px 1fr 300px; height: calc(100vh - 72px); overflow: hidden; }
 
 
 
 
37
 
38
  /* ── Header ── */
39
  header {
40
+ display: flex; align-items: center; justify-content: space-between;
41
+ padding: 16px 28px; border-bottom: 1px solid var(--border);
42
+ background: rgba(7,7,15,0.9); backdrop-filter: blur(12px);
43
+ position: sticky; top: 0; z-index: 100;
 
 
 
 
 
 
44
  }
45
+ .logo { display: flex; align-items: center; gap: 12px; }
 
 
 
 
 
 
46
  .logo-icon {
47
+ width: 36px; height: 36px; border-radius: 8px;
 
48
  background: linear-gradient(135deg, var(--accent), var(--accent2));
49
+ display: flex; align-items: center; justify-content: center; font-size: 18px;
 
 
 
 
50
  }
51
+ .logo-name { font-size: 15px; font-weight: 800; letter-spacing: -0.3px; }
52
+ .logo-sub { font-family: var(--mono); font-size: 10px; color: var(--text-dim); letter-spacing: 1.5px; margin-top: 2px; }
53
+ .header-right { display: flex; align-items: center; gap: 16px; }
54
+ .status-pill {
55
+ display: flex; align-items: center; gap: 6px;
56
+ font-family: var(--mono); font-size: 11px; color: var(--accent3);
57
+ background: rgba(67,233,123,0.08); border: 1px solid rgba(67,233,123,0.2);
58
+ padding: 4px 12px; border-radius: 20px;
59
  }
60
+ .pulse { width: 7px; height: 7px; background: var(--accent3); border-radius: 50%; animation: pulse 2s infinite; }
61
+ @keyframes pulse { 0%,100%{opacity:1;transform:scale(1)} 50%{opacity:0.4;transform:scale(0.7)} }
62
+ .overall-badge {
63
+ font-family: var(--mono); font-size: 12px; font-weight: 700;
64
+ color: var(--accent); background: rgba(108,99,255,0.1);
65
+ border: 1px solid rgba(108,99,255,0.3); padding: 4px 12px; border-radius: 20px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
66
  }
67
 
68
  /* ── Sidebar ── */
69
+ .sidebar { border-right: 1px solid var(--border); padding: 16px 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
70
+ .domain-label {
71
+ font-family: var(--mono); font-size: 9px; color: var(--text-dim);
72
+ letter-spacing: 2px; text-transform: uppercase; margin-bottom: 6px;
73
+ display: flex; align-items: center; gap: 6px;
 
 
 
 
 
 
 
 
 
 
 
74
  }
75
+ .domain-label::after { content: ''; flex: 1; height: 1px; background: var(--border); }
76
  .task-card {
77
+ background: var(--surface); border: 1px solid var(--border);
78
+ border-radius: var(--radius); padding: 10px 12px; cursor: pointer;
79
+ transition: all 0.15s; position: relative; overflow: hidden; margin-bottom: 4px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
80
  }
81
+ .task-card::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 3px; opacity: 0; transition: opacity 0.15s; }
82
+ .task-card:hover { border-color: rgba(108,99,255,0.4); background: var(--surface2); }
83
+ .task-card.active { border-color: var(--accent); background: var(--surface2); }
84
  .task-card.active::before { opacity: 1; }
85
+ .task-name { font-size: 12px; font-weight: 700; margin-bottom: 4px; }
86
+ .task-meta { display: flex; align-items: center; gap: 6px; }
87
+ .badge { font-family: var(--mono); font-size: 9px; padding: 2px 6px; border-radius: 4px; font-weight: 700; letter-spacing: 0.5px; }
88
+ .badge-easy { background: rgba(67,233,123,0.12); color: var(--accent3); }
89
+ .badge-medium { background: rgba(247,151,30,0.12); color: var(--accent4); }
90
+ .badge-hard { background: rgba(255,101,132,0.12); color: var(--accent2); }
91
+ .badge-veryhard { background: rgba(108,99,255,0.15); color: var(--accent); }
92
+ .task-score { font-family: var(--mono); font-size: 10px; color: var(--text-dim); margin-left: auto; }
93
+
94
+ /* Domain colors for left bar */
95
+ .domain-support .task-card::before { background: var(--accent); }
96
+ .domain-legal .task-card::before { background: var(--accent4); }
97
+ .domain-clinical .task-card::before { background: var(--accent2); }
98
+ .domain-engineering .task-card::before { background: var(--accent5); }
99
+
100
+ /* ── Center ── */
101
+ .center { display: flex; flex-direction: column; overflow: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  .center-header {
103
+ padding: 16px 24px; border-bottom: 1px solid var(--border);
104
+ display: flex; align-items: center; justify-content: space-between; flex-shrink: 0;
105
+ }
106
+ .center-title { font-size: 18px; font-weight: 800; }
107
+ .center-sub { font-family: var(--mono); font-size: 11px; color: var(--text-dim); margin-top: 3px; }
108
+ .action-bar { display: flex; gap: 8px; }
109
+ .btn { font-family: var(--mono); font-size: 11px; padding: 7px 16px; border-radius: 8px; border: none; cursor: pointer; transition: all 0.15s; font-weight: 700; letter-spacing: 0.5px; }
110
+ .btn-primary { background: var(--accent); color: #fff; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  .btn-primary:hover { background: #7c74ff; transform: translateY(-1px); }
112
+ .btn-secondary { background: var(--surface2); color: var(--text); border: 1px solid var(--border); }
 
 
 
 
 
 
113
  .btn-secondary:hover { border-color: var(--accent); }
114
+ .center-body { flex: 1; overflow-y: auto; padding: 20px 24px; }
115
+
116
+ /* ── Ticket card ── */
117
+ .ticket-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 14px; animation: slideIn 0.25s ease; }
118
+ @keyframes slideIn { from{opacity:0;transform:translateY(10px)} to{opacity:1;transform:translateY(0)} }
119
+ .ticket-id { font-family: var(--mono); font-size: 10px; color: var(--accent); margin-bottom: 4px; }
120
+ .ticket-subject { font-size: 16px; font-weight: 700; line-height: 1.3; margin-bottom: 12px; }
121
+ .ticket-body { font-size: 13px; line-height: 1.7; color: #b0b0cc; padding: 14px; background: var(--surface2); border-radius: 8px; border-left: 3px solid var(--border); margin-bottom: 14px; }
122
+ .ticket-meta { display: flex; gap: 10px; flex-wrap: wrap; }
123
+ .meta-chip { font-family: var(--mono); font-size: 10px; color: var(--text-dim); background: var(--surface2); padding: 3px 8px; border-radius: 5px; border: 1px solid var(--border); }
124
+ .tier-enterprise { color: var(--accent); border-color: rgba(108,99,255,0.3); }
125
+ .tier-pro { color: var(--accent4); border-color: rgba(247,151,30,0.3); }
126
+
127
+ /* ── Domain cards (legal/clinical/pr) ── */
128
+ .domain-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-bottom: 14px; animation: slideIn 0.25s ease; }
129
+ .domain-card-label { font-family: var(--mono); font-size: 10px; color: var(--text-dim); letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 6px; }
130
+ .domain-card-title { font-size: 15px; font-weight: 700; margin-bottom: 12px; line-height: 1.4; }
131
+ .domain-card-body { font-size: 13px; line-height: 1.7; color: #b0b0cc; padding: 14px; background: var(--surface2); border-radius: 8px; border-left: 3px solid var(--border); }
132
+ .code-diff { font-family: var(--mono); font-size: 11px; background: #0a0a14; border: 1px solid var(--border); border-radius: 8px; padding: 14px; overflow-x: auto; line-height: 1.6; white-space: pre; }
133
+ .diff-add { color: var(--accent3); }
134
+ .diff-remove { color: var(--accent2); }
135
+ .diff-header { color: var(--text-dim); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  /* ── Action form ── */
138
+ .action-form { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 20px; margin-top: 14px; }
139
+ .form-title { font-weight: 700; font-size: 14px; margin-bottom: 14px; }
140
+ .form-row { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; margin-bottom: 10px; }
141
+ .form-group { display: flex; flex-direction: column; gap: 5px; }
142
+ .form-label { font-family: var(--mono); font-size: 9px; color: var(--text-dim); letter-spacing: 1px; text-transform: uppercase; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
143
  .form-select, .form-textarea, .form-input {
144
+ background: var(--surface2); border: 1px solid var(--border); border-radius: 7px;
145
+ color: var(--text); font-family: var(--mono); font-size: 12px; padding: 8px 10px;
146
+ transition: border-color 0.15s; width: 100%;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
147
  }
148
+ .form-select:focus, .form-textarea:focus, .form-input:focus { outline: none; border-color: var(--accent); }
149
+ .form-textarea { font-family: var(--sans); font-size: 13px; resize: vertical; min-height: 100px; line-height: 1.6; }
150
+ .submit-row { display: flex; justify-content: flex-end; margin-top: 14px; }
151
 
152
  /* ── Right panel ── */
153
+ .right-panel { border-left: 1px solid var(--border); padding: 16px 14px; overflow-y: auto; display: flex; flex-direction: column; gap: 16px; }
154
+ .panel-label { font-family: var(--mono); font-size: 9px; color: var(--text-dim); letter-spacing: 2px; text-transform: uppercase; margin-bottom: 10px; }
155
+ .score-card { background: var(--surface); border: 1px solid var(--border); border-radius: var(--radius); padding: 16px; }
156
+ .big-score { font-family: var(--mono); font-size: 42px; font-weight: 700; line-height: 1; }
157
+ .score-label { font-family: var(--mono); font-size: 10px; color: var(--text-dim); margin-top: 4px; }
158
+ .breakdown-row { display: flex; justify-content: space-between; align-items: center; padding: 6px 0; border-bottom: 1px solid rgba(37,37,56,0.5); }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  .breakdown-row:last-child { border-bottom: none; }
160
+ .breakdown-label { font-family: var(--mono); font-size: 10px; color: var(--text-dim); }
161
+ .breakdown-val { font-family: var(--mono); font-size: 11px; font-weight: 700; }
162
+ .val-good { color: var(--accent3); } .val-mid { color: var(--accent4); } .val-bad { color: var(--accent2); }
163
+ .mini-chart { height: 50px; display: flex; align-items: flex-end; gap: 2px; }
164
+ .bar { flex: 1; border-radius: 2px 2px 0 0; min-height: 3px; transition: height 0.3s ease; }
165
+ .log-entry { font-family: var(--mono); font-size: 10px; color: var(--text-dim); padding: 5px 8px; border-radius: 4px; margin-bottom: 3px; background: var(--surface); }
166
+ .log-pos { color: var(--accent3); } .log-neg { color: var(--accent2); }
167
+
168
+ /* ── Queue ── */
169
+ .queue-item { display: flex; align-items: center; gap: 8px; padding: 8px 10px; background: var(--surface); border: 1px solid var(--border); border-radius: 7px; margin-bottom: 5px; font-size: 11px; }
170
+ .priority-dot { width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
171
+ .p1 { background: var(--accent2); box-shadow: 0 0 5px var(--accent2); } .p2 { background: var(--accent4); } .p3 { background: var(--accent3); } .p4 { background: var(--text-dim); }
172
+ .queue-subject { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
 
174
  /* ── Episode done ── */
175
+ .episode-done { background: linear-gradient(135deg, var(--surface2), rgba(108,99,255,0.06)); border: 1px solid rgba(108,99,255,0.25); border-radius: var(--radius); padding: 28px; text-align: center; animation: slideIn 0.35s ease; }
176
+ .done-score { font-family: var(--mono); font-size: 56px; font-weight: 700; color: var(--accent); }
177
+ .done-title { font-size: 20px; font-weight: 800; margin-bottom: 6px; }
178
+ .empty-state { display: flex; flex-direction: column; align-items: center; justify-content: center; height: 280px; color: var(--text-dim); text-align: center; gap: 10px; }
179
+ .empty-icon { font-size: 44px; opacity: 0.25; }
 
 
 
 
 
 
 
180
 
181
+ /* ── Agent cards ── */
182
+ .agent-card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 10px; margin-bottom: 6px; }
183
+ .agent-name { font-size: 12px; font-weight: 700; margin-bottom: 6px; }
184
+ .load-bar-track { height: 5px; background: var(--surface2); border-radius: 3px; overflow: hidden; margin-top: 5px; }
185
+ .load-bar-fill { height: 100%; border-radius: 3px; background: linear-gradient(90deg, var(--accent3), var(--accent)); transition: width 0.4s; }
186
+ .load-bar-fill.over { background: linear-gradient(90deg, var(--accent4), var(--accent2)); }
187
+
188
+ /* ── Stats bar ── */
189
+ .stats-bar { display: grid; grid-template-columns: repeat(3, 1fr); gap: 8px; margin-bottom: 14px; }
190
+ .stat-box { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 12px; text-align: center; }
191
+ .stat-num { font-family: var(--mono); font-size: 26px; font-weight: 700; }
192
+ .stat-label { font-family: var(--mono); font-size: 9px; color: var(--text-dim); letter-spacing: 1px; margin-top: 3px; }
193
+
194
+ ::-webkit-scrollbar { width: 4px; } ::-webkit-scrollbar-track { background: transparent; } ::-webkit-scrollbar-thumb { background: var(--border); border-radius: 2px; }
195
  </style>
196
  </head>
197
  <body>
198
  <div class="layout">
199
  <header>
200
  <div class="logo">
201
+ <div class="logo-icon">🌐</div>
202
  <div>
203
+ <div class="logo-name">OpenEnv — Multi-Domain</div>
204
+ <div class="logo-sub">AI AGENT EVALUATION ENVIRONMENT</div>
205
  </div>
206
  </div>
207
+ <div class="header-right">
208
+ <div class="overall-badge">OVERALL: <span id="overall-score">—</span></div>
209
+ <div class="status-pill"><div class="pulse"></div><span id="server-status">CONNECTING...</span></div>
210
  </div>
211
  </header>
212
 
213
  <div class="main">
214
  <!-- Sidebar -->
215
  <aside class="sidebar">
216
+
217
+ <div class="domain-support">
218
+ <div class="domain-label">🎫 Customer Support</div>
219
+ <div class="task-card active" id="tc-card" onclick="selectTask('ticket_classification')">
220
  <div class="task-name">Ticket Classification</div>
221
+ <div class="task-meta"><span class="badge badge-easy">EASY</span><span class="task-score" id="score-ticket_classification">—</span></div>
 
 
 
 
222
  </div>
223
+ <div class="task-card" id="rd-card" onclick="selectTask('response_drafting')">
224
  <div class="task-name">Response Drafting</div>
225
+ <div class="task-meta"><span class="badge badge-medium">MEDIUM</span><span class="task-score" id="score-response_drafting">—</span></div>
 
 
 
 
226
  </div>
227
+ <div class="task-card" id="qm-card" onclick="selectTask('queue_management')">
228
+ <div class="task-name">SLA Queue Management</div>
229
+ <div class="task-meta"><span class="badge badge-hard">HARD</span><span class="task-score" id="score-queue_management">—</span></div>
230
+ </div>
231
+ <div class="task-card" id="mt-card" onclick="selectTask('multi_turn_conversation')">
232
+ <div class="task-name">Multi-Turn De-escalation</div>
233
+ <div class="task-meta"><span class="badge badge-veryhard">VERY HARD</span><span class="task-score" id="score-multi_turn_conversation">—</span></div>
234
+ </div>
235
+ </div>
236
+
237
+ <div class="domain-legal">
238
+ <div class="domain-label">⚖️ Legal Review</div>
239
+ <div class="task-card" id="lci-card" onclick="selectTask('legal_clause_identification')">
240
+ <div class="task-name">Clause Identification</div>
241
+ <div class="task-meta"><span class="badge badge-easy">EASY</span><span class="task-score" id="score-legal_clause_identification">—</span></div>
242
+ </div>
243
+ <div class="task-card" id="lrf-card" onclick="selectTask('legal_risk_flagging')">
244
+ <div class="task-name">Risk Flagging</div>
245
+ <div class="task-meta"><span class="badge badge-medium">MEDIUM</span><span class="task-score" id="score-legal_risk_flagging">—</span></div>
246
+ </div>
247
+ <div class="task-card" id="lcr-card" onclick="selectTask('legal_clause_redlining')">
248
+ <div class="task-name">Clause Redlining</div>
249
+ <div class="task-meta"><span class="badge badge-hard">HARD</span><span class="task-score" id="score-legal_clause_redlining">—</span></div>
250
+ </div>
251
+ </div>
252
+
253
+ <div class="domain-clinical">
254
+ <div class="domain-label">🏥 Clinical Triage</div>
255
+ <div class="task-card" id="ctc-card" onclick="selectTask('clinical_triage_classification')">
256
+ <div class="task-name">Body System Classification</div>
257
+ <div class="task-meta"><span class="badge badge-easy">EASY</span><span class="task-score" id="score-clinical_triage_classification">—</span></div>
258
+ </div>
259
+ <div class="task-card" id="esi-card" onclick="selectTask('clinical_esi_assignment')">
260
+ <div class="task-name">ESI Level Assignment</div>
261
+ <div class="task-meta"><span class="badge badge-medium">MEDIUM</span><span class="task-score" id="score-clinical_esi_assignment">—</span></div>
262
+ </div>
263
+ <div class="task-card" id="ctn-card" onclick="selectTask('clinical_triage_note')">
264
+ <div class="task-name">Triage Note Generation</div>
265
+ <div class="task-meta"><span class="badge badge-hard">HARD</span><span class="task-score" id="score-clinical_triage_note">—</span></div>
266
+ </div>
267
+ </div>
268
+
269
+ <div class="domain-engineering">
270
+ <div class="domain-label">💻 PR Review</div>
271
+ <div class="task-card" id="ptc-card" onclick="selectTask('pr_type_classification')">
272
+ <div class="task-name">PR Type Classification</div>
273
+ <div class="task-meta"><span class="badge badge-easy">EASY</span><span class="task-score" id="score-pr_type_classification">—</span></div>
274
+ </div>
275
+ <div class="task-card" id="pbi-card" onclick="selectTask('pr_bug_identification')">
276
+ <div class="task-name">Bug Identification</div>
277
+ <div class="task-meta"><span class="badge badge-medium">MEDIUM</span><span class="task-score" id="score-pr_bug_identification">—</span></div>
278
+ </div>
279
+ <div class="task-card" id="prc-card" onclick="selectTask('pr_review_comment')">
280
+ <div class="task-name">Code Review Comment</div>
281
+ <div class="task-meta"><span class="badge badge-hard">HARD</span><span class="task-score" id="score-pr_review_comment">—</span></div>
282
  </div>
283
  </div>
284
 
285
  <div>
286
+ <div class="domain-label">Episode Log</div>
287
+ <div id="log-container" style="max-height:200px;overflow-y:auto;"></div>
288
  </div>
289
 
290
  <div>
291
+ <div class="domain-label">API Links</div>
292
+ <a href="/docs" target="_blank" style="display:flex;align-items:center;gap:8px;color:var(--accent);font-size:12px;text-decoration:none;padding:8px;background:var(--surface);border-radius:8px;border:1px solid var(--border);margin-bottom:5px;">📖 Swagger API Docs</a>
293
+ <a href="/tasks" target="_blank" style="display:flex;align-items:center;gap:8px;color:var(--text-dim);font-size:12px;text-decoration:none;padding:8px;background:var(--surface);border-radius:8px;border:1px solid var(--border);">📋 /tasks endpoint</a>
 
 
 
 
294
  </div>
295
  </aside>
296
 
 
299
  <div class="center-header">
300
  <div>
301
  <div class="center-title" id="center-title">Ticket Classification</div>
302
+ <div class="center-sub">Step <span id="step-counter">0</span> / <span id="max-steps">10</span> &nbsp;|&nbsp; <span id="domain-label">Customer Support</span></div>
 
 
303
  </div>
304
  <div class="action-bar">
305
+ <button class="btn btn-secondary" onclick="runBaseline()">▶ Run Heuristic Baseline</button>
306
  <button class="btn btn-primary" onclick="resetEpisode()">↺ Reset Episode</button>
307
  </div>
308
  </div>
 
309
  <div class="center-body" id="center-body">
310
  <div class="empty-state">
311
+ <div class="empty-icon">🌐</div>
312
+ <div style="font-size:14px;font-weight:700;">Multi-Domain AI Agent Environment</div>
313
+ <div style="font-size:12px;max-width:300px;line-height:1.6;">13 tasks across 4 professional domains. Select a task from the sidebar and click Reset Episode to begin.</div>
314
  </div>
315
  </div>
316
  </main>
 
318
  <!-- Right panel -->
319
  <aside class="right-panel">
320
  <div>
321
+ <div class="panel-label">Episode Score</div>
322
  <div class="score-card">
 
323
  <div class="big-score" id="current-score" style="color:var(--accent)">—</div>
324
  <div class="score-label" id="score-status">No active episode</div>
325
+ <div style="margin-top:12px;" id="score-breakdown"></div>
 
326
  </div>
327
  </div>
 
328
  <div>
329
+ <div class="panel-label">Reward History</div>
330
  <div class="score-card">
331
  <div class="mini-chart" id="reward-chart"></div>
332
+ <div style="display:flex;justify-content:space-between;margin-top:5px;font-family:var(--mono);font-size:9px;color:var(--text-dim);">
333
  <span>+1.0</span><span>avg: <span id="avg-reward">—</span></span><span>-1.0</span>
334
  </div>
335
  </div>
336
  </div>
 
337
  <div id="agents-section" style="display:none;">
338
+ <div class="panel-label">Agent Status</div>
339
  <div id="agents-container"></div>
340
  </div>
 
341
  <div id="queue-section" style="display:none;">
342
+ <div class="panel-label">Ticket Queue</div>
343
  <div id="queue-container"></div>
344
  </div>
345
  </aside>
 
351
  let currentTaskId = 'ticket_classification';
352
  let currentObs = null;
353
  let rewardHistory = [];
 
354
 
355
  const TASK_CONFIG = {
356
+ ticket_classification: { title: 'Ticket Classification', domain: 'Customer Support', maxSteps: 10, cardId: 'tc-card' },
357
+ response_drafting: { title: 'Response Drafting', domain: 'Customer Support', maxSteps: 6, cardId: 'rd-card' },
358
+ queue_management: { title: 'SLA Queue Management', domain: 'Customer Support', maxSteps: 40, cardId: 'qm-card' },
359
+ multi_turn_conversation: { title: 'Multi-Turn De-escalation', domain: 'Customer Support', maxSteps: 8, cardId: 'mt-card' },
360
+ legal_clause_identification: { title: 'Legal Clause Identification', domain: 'Legal Review', maxSteps: 10, cardId: 'lci-card' },
361
+ legal_risk_flagging: { title: 'Legal Risk Flagging', domain: 'Legal Review', maxSteps: 10, cardId: 'lrf-card' },
362
+ legal_clause_redlining: { title: 'Legal Clause Redlining', domain: 'Legal Review', maxSteps: 5, cardId: 'lcr-card' },
363
+ clinical_triage_classification: { title: 'Clinical Triage Classification', domain: 'Clinical Triage', maxSteps: 10, cardId: 'ctc-card' },
364
+ clinical_esi_assignment: { title: 'ESI Level Assignment', domain: 'Clinical Triage', maxSteps: 10, cardId: 'esi-card' },
365
+ clinical_triage_note: { title: 'Triage Note Generation', domain: 'Clinical Triage', maxSteps: 5, cardId: 'ctn-card' },
366
+ pr_type_classification: { title: 'PR Type Classification', domain: 'PR Review', maxSteps: 10, cardId: 'ptc-card' },
367
+ pr_bug_identification: { title: 'PR Bug Identification', domain: 'PR Review', maxSteps: 5, cardId: 'pbi-card' },
368
+ pr_review_comment: { title: 'PR Code Review', domain: 'PR Review', maxSteps: 5, cardId: 'prc-card' },
369
  };
370
 
371
  async function api(method, path, body) {
 
379
  function selectTask(taskId) {
380
  currentTaskId = taskId;
381
  document.querySelectorAll('.task-card').forEach(c => c.classList.remove('active'));
 
 
 
 
 
382
  const cfg = TASK_CONFIG[taskId];
383
+ document.getElementById(cfg.cardId).classList.add('active');
384
  document.getElementById('center-title').textContent = cfg.title;
385
+ document.getElementById('domain-label').textContent = cfg.domain;
386
  document.getElementById('max-steps').textContent = cfg.maxSteps;
387
+ document.getElementById('step-counter').textContent = 0;
388
  document.getElementById('center-body').innerHTML = `
389
  <div class="empty-state">
390
+ <div class="empty-icon"></div>
391
+ <div style="font-size:13px;">Click "Reset Episode" to start this task</div>
392
  </div>`;
393
  rewardHistory = [];
394
  updateRewardChart();
 
399
  document.getElementById('queue-section').style.display = 'none';
400
  }
401
 
 
 
 
 
 
402
  async function resetEpisode() {
403
  rewardHistory = [];
 
404
  document.getElementById('log-container').innerHTML = '';
 
405
  try {
406
  const data = await api('POST', `/reset?task_id=${currentTaskId}`);
407
  currentObs = data.observation;
408
  document.getElementById('step-counter').textContent = currentObs.step || 0;
409
  renderObservation(currentObs);
410
+ addLog(` Reset — ${currentTaskId}`);
411
  updateAgents(currentObs.agents);
412
  updateQueue(currentObs.ticket_queue, currentObs.sla_status);
413
+ } catch(e) { addLog(`❌ ${e.message}`, 'neg'); }
 
 
414
  }
415
 
416
  function renderObservation(obs) {
417
+ if (obs.episode_done) { fetchGrader(); return; }
 
 
 
 
418
  const body = document.getElementById('center-body');
419
+ const ticket = obs.current_ticket;
420
+ const clause = obs.current_clause;
421
+ const patient = obs.current_patient;
422
+ const pr = obs.current_pr;
423
 
424
+ if (ticket) renderTicketView(obs, body);
425
+ else if (clause) renderLegalView(obs, body);
426
+ else if (patient) renderClinicalView(obs, body);
427
+ else if (pr) renderPRView(obs, body);
428
+ else body.innerHTML = `<div class="empty-state"><div class="empty-icon">✓</div><div>Step complete</div></div>`;
 
 
429
 
430
  updateAgents(obs.agents);
431
  updateQueue(obs.ticket_queue, obs.sla_status);
432
  }
433
 
434
+ function renderTicketView(obs, body) {
435
+ const t = obs.current_ticket;
436
+ const isQueue = currentTaskId === 'queue_management';
437
+ const isMT = currentTaskId === 'multi_turn_conversation';
438
+ const history = t.previous_interactions || [];
439
+
440
+ let historyHTML = '';
441
+ if (history.length > 0) {
442
+ historyHTML = `<div style="margin-bottom:12px;"><div style="font-family:var(--mono);font-size:10px;color:var(--text-dim);margin-bottom:8px;">CONVERSATION HISTORY</div>` +
443
+ history.map(m => `<div style="padding:8px;margin-bottom:6px;border-radius:6px;background:${m.role==='customer'?'var(--surface2)':'rgba(108,99,255,0.08)'};border:1px solid var(--border);font-size:12px;"><span style="font-family:var(--mono);font-size:9px;color:var(--text-dim);text-transform:uppercase;">${m.role}:</span> ${m.content}</div>`).join('') + `</div>`;
444
+ }
445
 
446
+ body.innerHTML = `
447
  <div class="ticket-card">
448
+ <div class="ticket-id">${t.ticket_id}</div>
449
+ <div class="ticket-subject">${t.subject}</div>
450
+ ${historyHTML}
451
+ <div class="ticket-body">${t.body}</div>
452
+ <div class="ticket-meta">
453
+ <span class="meta-chip tier-${t.customer_tier}">${t.customer_tier?.toUpperCase()}</span>
454
+ ${t.category ? `<span class="meta-chip">📁 ${t.category}</span>` : ''}
455
+ ${t.priority ? `<span class="meta-chip">🔺 ${t.priority}</span>` : ''}
456
+ <span class="meta-chip">😶 Sentiment: ${(t.sentiment_score||0).toFixed(2)}</span>
 
 
 
 
 
 
457
  </div>
458
+ </div>
459
+ ${obs.knowledge_base?.length ? renderKB(obs.knowledge_base) : ''}
460
+ ${renderActionForm(obs)}`;
461
+ }
462
+
463
+ function renderLegalView(obs, body) {
464
+ const c = obs.current_clause;
465
+ body.innerHTML = `
466
+ <div class="domain-card">
467
+ <div class="domain-card-label">${c.contract_type} — ${c.counterparty}</div>
468
+ <div class="domain-card-title">Clause ${c.clause_id}</div>
469
+ <div class="domain-card-body">${c.text}</div>
470
+ ${c.true_risk_level ? `<div style="margin-top:10px;font-family:var(--mono);font-size:11px;color:var(--accent4);">Known Risk Level: ${c.true_risk_level}</div>` : ''}
471
+ </div>
472
+ ${renderActionForm(obs)}`;
473
+ }
474
+
475
+ function renderClinicalView(obs, body) {
476
+ const p = obs.current_patient;
477
+ const vitals = p.vitals || {};
478
+ body.innerHTML = `
479
+ <div class="domain-card">
480
+ <div class="domain-card-label">Patient ${p.patient_id} — Age ${p.age} ${p.gender}</div>
481
+ <div class="domain-card-title">${p.chief_complaint}</div>
482
+ <div style="display:grid;grid-template-columns:repeat(5,1fr);gap:8px;margin:12px 0;">
483
+ ${Object.entries(vitals).map(([k,v]) => `<div style="background:var(--surface2);border:1px solid var(--border);border-radius:6px;padding:8px;text-align:center;"><div style="font-family:var(--mono);font-size:14px;font-weight:700;">${v}</div><div style="font-family:var(--mono);font-size:9px;color:var(--text-dim);margin-top:2px;">${k}</div></div>`).join('')}
484
  </div>
485
+ <div class="domain-card-body"><strong>History:</strong> ${p.history}</div>
486
+ ${p.true_esi_level ? `<div style="margin-top:10px;font-family:var(--mono);font-size:11px;color:var(--accent4);">Known ESI Level: ${p.true_esi_level}</div>` : ''}
487
+ </div>
488
+ ${renderActionForm(obs)}`;
489
  }
490
 
491
+ function renderPRView(obs, body) {
492
+ const pr = obs.current_pr;
493
+ const diffLines = (pr.diff||'').split('\n').map(line => {
494
+ if (line.startsWith('+')) return `<span class="diff-add">${escHtml(line)}</span>`;
495
+ if (line.startsWith('-')) return `<span class="diff-remove">${escHtml(line)}</span>`;
496
+ if (line.startsWith('@@')) return `<span class="diff-header">${escHtml(line)}</span>`;
497
+ return escHtml(line);
498
+ }).join('\n');
499
+ body.innerHTML = `
500
+ <div class="domain-card">
501
+ <div class="domain-card-label">PR ${pr.pr_id} — by ${pr.author}</div>
502
+ <div class="domain-card-title">${pr.title}</div>
503
+ <div class="domain-card-body" style="margin-bottom:12px;">${pr.description}</div>
504
+ <div class="code-diff">${diffLines}</div>
505
+ ${pr.true_bug_description ? `<div style="margin-top:10px;font-family:var(--mono);font-size:11px;color:var(--accent4);">Known Bug: ${pr.true_bug_description}</div>` : ''}
506
+ </div>
507
+ ${renderActionForm(obs)}`;
508
  }
509
 
510
+ function escHtml(s) { return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;'); }
511
+
512
+ function renderKB(articles) {
513
+ return `<div style="margin-bottom:14px;">
514
+ <div style="font-family:var(--mono);font-size:9px;color:var(--text-dim);letter-spacing:2px;text-transform:uppercase;margin-bottom:8px;">Knowledge Base</div>
515
+ ${articles.slice(0,2).map(a => `
516
+ <div style="background:var(--surface);border:1px solid var(--border);border-radius:8px;padding:12px;margin-bottom:6px;">
517
+ <div style="font-size:12px;font-weight:700;margin-bottom:4px;"><span style="font-family:var(--mono);font-size:10px;color:var(--accent);margin-right:8px;">${a.article_id}</span>${a.title}</div>
518
+ <div style="font-size:11px;color:var(--text-dim);line-height:1.5;">${a.content?.substring(0,200)}...</div>
519
+ </div>`).join('')}
520
+ </div>`;
521
+ }
522
+
523
+ function renderActionForm(obs) {
524
+ const tid = currentTaskId;
525
+ const actions = obs.valid_actions || [];
526
+
527
+ if (tid === 'ticket_classification') {
528
+ const t = obs.current_ticket;
529
+ return `<div class="action-form"><div class="form-title">⚡ Classify Ticket</div>
530
  <div class="form-row">
531
+ <div class="form-group"><label class="form-label">Category</label>
 
532
  <select class="form-select" id="f-category">
533
+ ${['billing','technical','account','feature_request','abuse','unknown'].map(c => `<option value="${c}">${c.replace('_',' ').toUpperCase()}</option>`).join('')}
534
+ </select></div>
535
+ <div class="form-group"><label class="form-label">Priority</label>
 
 
 
 
 
536
  <select class="form-select" id="f-priority">
537
+ ${['P1','P2','P3','P4'].map(p => `<option value="${p}">${p}</option>`).join('')}
538
+ </select></div>
 
 
 
 
 
 
 
 
 
 
 
539
  </div>
540
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'classify',ticket_id:'${t?.ticket_id}',category:document.getElementById('f-category').value,priority:document.getElementById('f-priority').value})">Submit →</button></div>
541
  </div>`;
542
+ }
543
 
544
+ if (tid === 'response_drafting') {
545
+ const t = obs.current_ticket;
546
+ return `<div class="action-form"><div class="form-title">✍️ Draft Response</div>
547
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Response Text</label>
548
+ <textarea class="form-textarea" id="f-response" placeholder="Write a professional customer response..."></textarea></div>
549
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'draft_response',ticket_id:'${t?.ticket_id}',response_text:document.getElementById('f-response').value})">Send →</button></div>
 
 
 
 
 
 
 
550
  </div>`;
551
+ }
 
 
 
 
552
 
553
+ if (tid === 'queue_management') {
554
+ const queue = obs.ticket_queue || [];
555
+ return `<div class="action-form"><div class="form-title">⚙️ Queue Action</div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
556
  <div class="form-row">
557
+ <div class="form-group"><label class="form-label">Ticket</label>
 
558
  <select class="form-select" id="f-ticket-id">
559
+ ${queue.map(t => `<option value="${t.ticket_id}">${t.ticket_id} | ${t.priority||'?'} | ${t.subject?.substring(0,30)}</option>`).join('')}
560
+ </select></div>
561
+ <div class="form-group"><label class="form-label">Action</label>
562
+ <select class="form-select" id="f-action-type" onchange="toggleAgentRow()">
563
+ ${['assign_ticket','resolve','escalate','close','no_op'].map(a => `<option value="${a}">${a.replace('_',' ').toUpperCase()}</option>`).join('')}
564
+ </select></div>
 
 
 
 
 
 
565
  </div>
566
+ <div id="agent-row" class="form-group" style="margin-bottom:10px;"><label class="form-label">Assign To</label>
 
567
  <select class="form-select" id="f-agent-id">
568
+ <option value="agent_billing">agent_billing — Billing Specialist</option>
569
+ <option value="agent_tech">agent_tech — Technical Support</option>
570
+ <option value="agent_general">agent_general — General Support</option>
571
+ </select></div>
572
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitQueueAction()">Execute →</button></div>
573
+ </div>`;
574
+ }
575
+
576
+ if (tid === 'multi_turn_conversation') {
577
+ const t = obs.current_ticket;
578
+ return `<div class="action-form"><div class="form-title">💬 Respond or Escalate</div>
579
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Response</label>
580
+ <textarea class="form-textarea" id="f-mt-response" placeholder="Write your response to the customer..."></textarea></div>
581
+ <div class="submit-row" style="gap:8px;">
582
+ <button class="btn btn-secondary" onclick="submitAction({action_type:'escalate',ticket_id:'${t?.ticket_id}'})">↑ Escalate</button>
583
+ <button class="btn btn-secondary" onclick="submitAction({action_type:'resolve',ticket_id:'${t?.ticket_id}'})">✓ Resolve</button>
584
+ <button class="btn btn-primary" onclick="submitAction({action_type:'draft_response',ticket_id:'${t?.ticket_id}',response_text:document.getElementById('f-mt-response').value})">Send →</button>
585
  </div>
586
+ </div>`;
587
+ }
588
+
589
+ if (tid === 'legal_clause_identification') {
590
+ return `<div class="action-form"><div class="form-title">⚖️ Identify Clause Type</div>
591
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Clause Type</label>
592
+ <select class="form-select" id="f-clause-type">
593
+ ${['indemnity','liability','ip','termination','unknown'].map(c => `<option value="${c}">${c.toUpperCase()}</option>`).join('')}
594
+ </select></div>
595
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'identify_clause',clause_type:document.getElementById('f-clause-type').value})">Submit →</button></div>
596
+ </div>`;
597
+ }
598
+
599
+ if (tid === 'legal_risk_flagging') {
600
+ return `<div class="action-form"><div class="form-title">🚩 Flag Risk Level</div>
601
+ <div class="form-row">
602
+ <div class="form-group"><label class="form-label">Risk Level</label>
603
+ <select class="form-select" id="f-risk-level">
604
+ ${['low','medium','high','critical'].map(r => `<option value="${r}">${r.toUpperCase()}</option>`).join('')}
605
+ </select></div>
606
+ <div class="form-group"><label class="form-label">Reasoning</label>
607
+ <input class="form-input" id="f-risk-reason" placeholder="Brief justification..."/></div>
608
  </div>
609
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'flag_risk',risk_level:document.getElementById('f-risk-level').value,reasoning:document.getElementById('f-risk-reason').value})">Submit →</button></div>
610
  </div>`;
611
+ }
612
 
613
+ if (tid === 'legal_clause_redlining') {
614
+ return `<div class="action-form"><div class="form-title">✏️ Redline Clause</div>
615
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Improved Clause Text</label>
616
+ <textarea class="form-textarea" id="f-redline" placeholder="Write the improved, lower-risk version of this clause..."></textarea></div>
617
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'redline',redline_text:document.getElementById('f-redline').value})">Submit →</button></div>
618
+ </div>`;
619
  }
 
620
 
621
+ if (tid === 'clinical_triage_classification') {
622
+ return `<div class="action-form"><div class="form-title">🏥 Classify Body System</div>
623
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Body System</label>
624
+ <select class="form-select" id="f-body-system">
625
+ ${['cardiac','respiratory','neurologic','gi','musculoskeletal','other'].map(s => `<option value="${s}">${s.toUpperCase()}</option>`).join('')}
626
+ </select></div>
627
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'classify_triage',body_system:document.getElementById('f-body-system').value})">Submit →</button></div>
628
+ </div>`;
629
+ }
630
 
631
+ if (tid === 'clinical_esi_assignment') {
632
+ return `<div class="action-form"><div class="form-title">🔢 Assign ESI Level</div>
633
+ <div class="form-row">
634
+ <div class="form-group"><label class="form-label">ESI Level (1=Critical, 5=Non-urgent)</label>
635
+ <select class="form-select" id="f-esi">
636
+ ${[1,2,3,4,5].map(n => `<option value="${n}">ESI ${n}</option>`).join('')}
637
+ </select></div>
638
+ <div class="form-group"><label class="form-label">Reasoning</label>
639
+ <input class="form-input" id="f-esi-reason" placeholder="Clinical justification..."/></div>
640
+ </div>
641
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'assign_esi',esi_level:parseInt(document.getElementById('f-esi').value),reasoning:document.getElementById('f-esi-reason').value})">Submit →</button></div>
642
+ </div>`;
643
+ }
644
 
645
+ if (tid === 'clinical_triage_note') {
646
+ return `<div class="action-form"><div class="form-title">📋 Write Triage Note</div>
647
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Clinical Triage Note</label>
648
+ <textarea class="form-textarea" id="f-note" placeholder="Patient presents with... Immediate actions: stat IV access, oxygen, continuous monitor..."></textarea></div>
649
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'write_triage_note',triage_note:document.getElementById('f-note').value})">Submit →</button></div>
650
+ </div>`;
651
+ }
652
+
653
+ if (tid === 'pr_type_classification') {
654
+ return `<div class="action-form"><div class="form-title">🔍 Classify PR Type</div>
655
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">PR Type</label>
656
+ <select class="form-select" id="f-pr-type">
657
+ ${['bug_fix','feature','refactor','security'].map(t => `<option value="${t}">${t.replace('_',' ').toUpperCase()}</option>`).join('')}
658
+ </select></div>
659
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'classify_pr',pr_type:document.getElementById('f-pr-type').value})">Submit →</button></div>
660
+ </div>`;
661
+ }
662
+
663
+ if (tid === 'pr_bug_identification') {
664
+ return `<div class="action-form"><div class="form-title">🐛 Identify Bug</div>
665
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Bug Description</label>
666
+ <textarea class="form-textarea" id="f-bug" placeholder="Describe the specific vulnerability or bug found in this diff..."></textarea></div>
667
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'identify_bug',bug_description:document.getElementById('f-bug').value})">Submit →</button></div>
668
+ </div>`;
669
+ }
670
 
671
+ if (tid === 'pr_review_comment') {
672
+ return `<div class="action-form"><div class="form-title">💬 Write Review Comment</div>
673
+ <div class="form-group" style="margin-bottom:10px;"><label class="form-label">Review Comment</label>
674
+ <textarea class="form-textarea" id="f-review" placeholder="Approve or block this PR with your code review comment..."></textarea></div>
675
+ <div class="submit-row"><button class="btn btn-primary" onclick="submitAction({action_type:'review_pr',review_comment:document.getElementById('f-review').value})">Submit →</button></div>
676
+ </div>`;
677
+ }
678
 
679
+ return '';
 
 
 
 
680
  }
681
 
682
+ function toggleAgentRow() {
683
+ const action = document.getElementById('f-action-type')?.value;
684
+ const row = document.getElementById('agent-row');
685
+ if (row) row.style.display = action === 'assign_ticket' ? 'flex' : 'none';
686
+ }
687
+
688
+ function submitQueueAction() {
689
  const action_type = document.getElementById('f-action-type').value;
690
  const ticket_id = document.getElementById('f-ticket-id')?.value;
691
  const target_agent_id = document.getElementById('f-agent-id')?.value;
692
+ submitAction({ action_type, ticket_id, target_agent_id });
 
693
  }
694
 
695
  async function submitAction(actionBody) {
 
697
  const data = await api('POST', `/step?task_id=${currentTaskId}`, actionBody);
698
  currentObs = data.observation;
699
  const reward = data.reward;
 
700
  rewardHistory.push(reward.total);
701
  document.getElementById('step-counter').textContent = currentObs.step || rewardHistory.length;
 
702
  const sign = reward.total >= 0 ? '+' : '';
703
+ addLog(`Step ${rewardHistory.length}: ${sign}${reward.total.toFixed(3)}`, reward.total >= 0 ? 'pos' : 'neg');
 
704
  updateRewardChart();
705
  updateScoreDisplay(reward);
706
  renderObservation(currentObs);
707
+ if (data.done) fetchGrader();
708
+ } catch(e) { addLog(`❌ ${e.message}`, 'neg'); }
 
 
 
 
 
709
  }
710
 
711
+ async function fetchGrader() {
712
  try {
713
  const result = await api('POST', `/grader?task_id=${currentTaskId}`);
714
+ showDone(result);
715
+ document.getElementById(`score-${currentTaskId}`).textContent = result.final_score.toFixed(3);
716
+ updateOverallScore();
717
+ } catch(e) { console.error(e); }
 
 
718
  }
719
 
720
+ function showDone(result) {
 
721
  const score = result.final_score;
722
  const color = score >= 0.7 ? 'var(--accent3)' : score >= 0.4 ? 'var(--accent4)' : 'var(--accent2)';
723
  const emoji = score >= 0.7 ? '🏆' : score >= 0.4 ? '📊' : '📉';
 
724
  document.getElementById('center-body').innerHTML = `
725
  <div class="episode-done">
726
+ <div style="font-size:36px;margin-bottom:12px;">${emoji}</div>
727
  <div class="done-title">Episode Complete</div>
728
  <div class="done-score" style="color:${color}">${score.toFixed(4)}</div>
729
+ <div style="font-family:var(--mono);font-size:12px;color:var(--text-dim);margin:8px 0 20px">
730
+ ${result.passed ? '✓ PASSED' : '✗ DID NOT PASS'} &nbsp;|&nbsp; ${rewardHistory.length} steps
731
  </div>
732
+ ${Object.entries(result.metrics||{}).filter(([k,v])=>typeof v==='number').map(([k,v])=>`
733
+ <div class="breakdown-row">
734
+ <span class="breakdown-label">${k.replace(/_/g,' ')}</span>
735
+ <span class="breakdown-val ${v>=0.7?'val-good':v>=0.4?'val-mid':'val-bad'}">${v.toFixed(4)}</span>
736
+ </div>`).join('')}
737
+ <button class="btn btn-primary" style="margin-top:20px;" onclick="resetEpisode()">↺ New Episode</button>
 
 
738
  </div>`;
 
739
  document.getElementById('current-score').textContent = score.toFixed(4);
740
  document.getElementById('current-score').style.color = color;
741
+ document.getElementById('score-status').textContent = result.passed ? '✓ Passed' : '✗ Did not pass';
742
  }
743
 
744
  function updateScoreDisplay(reward) {
 
746
  const color = total >= 0.6 ? 'var(--accent3)' : total >= 0.2 ? 'var(--accent4)' : 'var(--accent2)';
747
  document.getElementById('current-score').textContent = total.toFixed(4);
748
  document.getElementById('current-score').style.color = color;
749
+ document.getElementById('score-status').textContent = 'Step reward';
 
750
  const bd = reward.breakdown || {};
751
  document.getElementById('score-breakdown').innerHTML = Object.entries(bd).map(([k,v]) => `
752
  <div class="breakdown-row">
753
  <span class="breakdown-label">${k.replace(/_/g,' ')}</span>
754
+ <span class="breakdown-val ${v>=0.6?'val-good':v<0?'val-bad':'val-mid'}">${typeof v==='number'?v.toFixed(3):v}</span>
755
  </div>`).join('');
756
  }
757
 
758
  function updateRewardChart() {
759
  const container = document.getElementById('reward-chart');
760
+ if (!rewardHistory.length) { container.innerHTML=''; return; }
761
+ const avg = (rewardHistory.reduce((a,b)=>a+b,0)/rewardHistory.length).toFixed(3);
 
762
  document.getElementById('avg-reward').textContent = avg;
 
763
  container.innerHTML = rewardHistory.map(r => {
764
+ const pct = ((r+1)/2*100).toFixed(1);
765
+ const color = r>=0.3?'var(--accent3)':r>=0?'var(--accent4)':'var(--accent2)';
766
  return `<div class="bar" style="height:${pct}%;background:${color};opacity:0.8;"></div>`;
767
  }).join('');
768
  }
769
 
770
  function updateAgents(agents) {
771
+ if (!agents?.length) { document.getElementById('agents-section').style.display='none'; return; }
772
+ document.getElementById('agents-section').style.display='block';
 
 
 
773
  document.getElementById('agents-container').innerHTML = agents.map(a => {
774
+ const pct = (a.current_load/a.max_load*100).toFixed(0);
775
+ return `<div class="agent-card">
776
+ <div class="agent-name">${a.name}</div>
777
+ <div style="font-family:var(--mono);font-size:9px;color:var(--text-dim);">Load: ${a.current_load}/${a.max_load}</div>
778
+ <div class="load-bar-track"><div class="load-bar-fill ${a.current_load>=a.max_load?'over':''}" style="width:${pct}%"></div></div>
779
+ </div>`;
 
 
 
 
 
 
780
  }).join('');
781
  }
782
 
783
  function updateQueue(queue, slaStatus) {
784
+ if (!queue?.length) { document.getElementById('queue-section').style.display='none'; return; }
785
+ document.getElementById('queue-section').style.display='block';
 
 
 
786
  document.getElementById('queue-container').innerHTML = queue.slice(0,8).map(t => {
787
+ const p = (t.priority||'P3').replace('_CRITICAL','').replace('_HIGH','').replace('_MEDIUM','').replace('_LOW','').toLowerCase();
788
+ return `<div class="queue-item">
789
+ <div class="priority-dot ${p}"></div>
790
+ <span class="queue-subject">${t.subject||t.ticket_id}</span>
791
+ </div>`;
 
 
 
792
  }).join('');
793
  }
794
 
795
  function addLog(message, type) {
796
  const container = document.getElementById('log-container');
797
  const div = document.createElement('div');
798
+ div.className = `log-entry ${type==='pos'?'log-pos':type==='neg'?'log-neg':''}`;
799
  div.textContent = message;
800
  container.insertBefore(div, container.firstChild);
801
+ if (container.children.length > 40) container.removeChild(container.lastChild);
802
  }
803
 
804
  async function runBaseline() {
805
+ addLog('⚡ Running heuristic baseline...');
806
  try {
807
  const data = await api('POST', '/baseline');
808
+ Object.entries(data.tasks||{}).forEach(([tid, result]) => {
809
+ const el = document.getElementById(`score-${tid}`);
810
+ if (el) el.textContent = result.final_score.toFixed(3);
811
+ addLog(`${tid.replace(/_/g,' ')}: ${result.final_score.toFixed(3)} ${result.passed?'✓':'✗'}`,
812
+ result.final_score >= 0.5 ? 'pos' : 'neg');
 
 
813
  });
814
+ document.getElementById('overall-score').textContent = data.overall_score.toFixed(3);
815
+ addLog(`Overall: ${data.overall_score.toFixed(3)}`, data.overall_score > 0.6 ? 'pos' : 'neg');
816
+ } catch(e) { addLog(`Baseline error: ${e.message}`, 'neg'); }
817
+ }
818
+
819
+ function updateOverallScore() {
820
+ const scores = Object.keys(TASK_CONFIG).map(tid => {
821
+ const el = document.getElementById(`score-${tid}`);
822
+ return el && el.textContent !== '—' ? parseFloat(el.textContent) : null;
823
+ }).filter(s => s !== null);
824
+ if (scores.length > 0) {
825
+ const avg = scores.reduce((a,b)=>a+b,0)/scores.length;
826
+ document.getElementById('overall-score').textContent = avg.toFixed(3);
827
  }
828
  }
829
 
830
+ // Health check on load
831
  (async () => {
832
  try {
833
+ const h = await api('GET', '/health');
834
  document.getElementById('server-status').textContent = 'LIVE';
835
+ addLog(`✓ Server online — ${h.tasks?.length||0} tasks`);
836
  } catch(e) {
837
  document.getElementById('server-status').textContent = 'OFFLINE';
838
  document.getElementById('server-status').style.color = 'var(--accent2)';
839
+ document.querySelector('.status-pill').style.color = 'var(--accent2)';
840
+ document.querySelector('.status-pill').style.borderColor = 'rgba(255,101,132,0.3)';
841
+ document.querySelector('.pulse').style.background = 'var(--accent2)';
842
  }
843
  })();
844
  </script>
845
  </body>
846
+ </html>