vinayumarbharwal commited on
Commit
5bc9ca8
·
1 Parent(s): 5cdf996
openenv_bug_triage/ui/app.js CHANGED
@@ -1,28 +1,39 @@
1
- const els = {
2
  taskSelect: document.getElementById("task-select"),
3
  seedInput: document.getElementById("seed-input"),
4
  resetBtn: document.getElementById("reset-btn"),
5
  stateBtn: document.getElementById("state-btn"),
6
  stepBtn: document.getElementById("step-btn"),
7
  status: document.getElementById("status"),
 
 
 
8
  actionType: document.getElementById("action-type"),
 
9
  payloadFields: document.getElementById("payload-fields"),
10
  actionPreview: document.getElementById("action-preview"),
11
  ticketMeta: document.getElementById("ticket-meta"),
 
 
 
 
 
 
 
12
  ticketBody: document.getElementById("ticket-body"),
 
13
  stepLog: document.getElementById("step-log"),
14
  stateJson: document.getElementById("state-json"),
15
  stepsUsed: document.getElementById("steps-used"),
16
  stepsRemaining: document.getElementById("steps-remaining"),
 
 
 
 
17
  doneFlag: document.getElementById("done-flag"),
18
  cumulativeReward: document.getElementById("cumulative-reward"),
19
  };
20
 
21
- let currentObservation = null;
22
- let done = false;
23
- let stepCount = 0;
24
- let cumulativeReward = 0;
25
-
26
  const DEFAULT_COMPONENTS = [
27
  "api-gateway",
28
  "auth-service",
@@ -45,24 +56,133 @@ const DEFAULT_TEAMS = [
45
  "data-platform",
46
  ];
47
 
48
- function setStatus(text, kind = "idle") {
49
- els.status.textContent = text;
50
- els.status.className = `status status-${kind}`;
51
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
 
53
  function pretty(obj) {
54
- return JSON.stringify(obj, null, 2);
55
  }
56
 
57
- function optionHtml(value, selectedValue) {
58
- const selected = value === selectedValue ? "selected" : "";
59
- return `<option value="${value}" ${selected}>${value}</option>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
60
  }
61
 
62
  function componentsList() {
63
- return currentObservation?.available_components?.length
64
- ? currentObservation.available_components
65
- : DEFAULT_COMPONENTS;
66
  }
67
 
68
  function teamsList() {
@@ -71,10 +191,148 @@ function teamsList() {
71
  : DEFAULT_TEAMS;
72
  }
73
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  function renderPayloadFields() {
75
  const actionType = els.actionType.value;
 
76
  const components = componentsList();
77
  const teams = teamsList();
 
78
 
79
  if (actionType === "classify") {
80
  els.payloadFields.innerHTML = `
@@ -99,63 +357,49 @@ function renderPayloadFields() {
99
  <label>
100
  <span>Component</span>
101
  <select id="f-component">
102
- ${components.map((c, i) => optionHtml(c, i === 0 ? c : "")).join("")}
103
  </select>
104
  </label>
105
  `;
106
- return;
107
- }
108
-
109
- if (actionType === "assign") {
110
  els.payloadFields.innerHTML = `
111
  <label>
112
  <span>Team</span>
113
  <select id="f-team">
114
- ${teams.map((t, i) => optionHtml(t, i === 0 ? t : "")).join("")}
115
  </select>
116
  </label>
117
  `;
118
- return;
119
- }
120
-
121
- if (actionType === "mark_duplicate") {
122
  els.payloadFields.innerHTML = `
123
  <label>
124
- <span>Canonical Ticket ID</span>
125
- <input id="f-canonical" type="text" placeholder="BUG-1001" />
126
  </label>
127
  `;
128
- return;
129
- }
130
-
131
- if (actionType === "request_info") {
132
  els.payloadFields.innerHTML = `
133
  <label>
134
- <span>Info Type</span>
135
  <select id="f-info-type">
136
- <option value="repro_steps">repro_steps</option>
137
- <option value="logs">logs</option>
138
- <option value="both" selected>both</option>
139
  </select>
140
  </label>
141
  `;
142
- return;
143
- }
144
-
145
- if (actionType === "defer") {
146
  els.payloadFields.innerHTML = `
147
  <label>
148
  <span>Reason</span>
149
- <input id="f-defer-reason" type="text" placeholder="Waiting for roadmap decision" />
150
  </label>
151
  `;
152
- return;
153
- }
154
-
155
- if (actionType === "close") {
156
  els.payloadFields.innerHTML = `
157
  <label>
158
- <span>Close Reason</span>
159
  <select id="f-close-reason">
160
  <option value="invalid">invalid</option>
161
  <option value="wont_fix">wont_fix</option>
@@ -164,20 +408,18 @@ function renderPayloadFields() {
164
  </select>
165
  </label>
166
  `;
167
- return;
168
- }
169
-
170
- if (actionType === "escalate_incident") {
171
  els.payloadFields.innerHTML = `
172
  <label>
173
  <span>Justification</span>
174
- <textarea id="f-justification" placeholder="Production impact and SLA risk detected."></textarea>
175
  </label>
176
  `;
177
- return;
 
178
  }
179
 
180
- els.payloadFields.innerHTML = `<p class="ticket-meta">No payload required.</p>`;
181
  }
182
 
183
  function buildAction() {
@@ -222,124 +464,306 @@ function buildAction() {
222
  }
223
 
224
  function renderPreview() {
225
- const action = buildAction();
226
- els.actionPreview.textContent = pretty(action);
227
  }
228
 
229
- function renderObservation(obs) {
230
- currentObservation = obs;
231
- if (!obs || !obs.current_ticket) {
232
- els.ticketMeta.textContent = "No active ticket";
233
- els.ticketBody.textContent = "{}";
234
- els.stepsUsed.textContent = "0";
235
- els.stepsRemaining.textContent = "0";
 
236
  return;
237
  }
238
 
239
- const t = obs.current_ticket;
240
- els.ticketMeta.textContent = `${t.ticket_id} | ${t.service} | ${t.reporter_type} | tier:${t.customer_tier}`;
241
- els.ticketBody.textContent = pretty(t);
242
-
243
- els.stepsUsed.textContent = String(obs.steps_used);
244
- els.stepsRemaining.textContent = String(obs.steps_remaining);
245
-
246
- renderPayloadFields();
247
- renderPreview();
 
 
 
 
 
 
248
  }
249
 
250
- function appendLog(text) {
251
  const li = document.createElement("li");
252
- li.textContent = text;
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  els.stepLog.prepend(li);
254
  }
255
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  async function api(url, method = "GET", body = null) {
257
- const opts = { method, headers: {} };
258
  if (body !== null) {
259
- opts.headers["Content-Type"] = "application/json";
260
- opts.body = JSON.stringify(body);
261
  }
262
 
263
- const res = await fetch(url, opts);
264
- const data = await res.json().catch(() => null);
265
- if (!res.ok) {
266
- const msg = data?.detail || `HTTP ${res.status}`;
267
- throw new Error(msg);
268
  }
269
  return data;
270
  }
271
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
272
  async function doReset() {
273
- const task_id = els.taskSelect.value;
274
  const seed = Number(els.seedInput.value || 42);
275
 
276
  try {
 
 
277
  setStatus("Resetting episode...", "idle");
278
- const obs = await api("/reset", "POST", { task_id, seed });
 
279
  done = false;
280
  stepCount = 0;
281
  cumulativeReward = 0;
282
  els.stepLog.innerHTML = "";
283
- renderObservation(obs);
284
- els.doneFlag.textContent = "false";
285
- els.cumulativeReward.textContent = "0.00";
286
- setStatus(`Ready on ${obs.current_ticket?.ticket_id || "no-ticket"}`, "ok");
287
- appendLog(`reset task=${task_id} seed=${seed}`);
288
- } catch (err) {
289
- setStatus(`Reset failed: ${err.message}`, "error");
 
 
 
 
 
 
 
 
 
 
 
 
 
 
290
  }
291
  }
292
 
293
  async function doStep() {
294
- if (!currentObservation) {
295
- setStatus("Run Reset first.", "error");
296
  return;
297
  }
 
298
  if (done) {
299
- setStatus("Episode already done. Reset to start another.", "error");
300
  return;
301
  }
302
 
303
  const action = buildAction();
304
 
305
  try {
 
 
 
 
306
  const result = await api("/step", "POST", { action });
307
  stepCount += 1;
308
  done = Boolean(result.done);
309
-
310
- const reward = Number(result.reward?.step_reward || 0);
311
  cumulativeReward = Number(result.reward?.cumulative_reward || cumulativeReward);
312
 
313
  renderObservation(result.observation);
314
- els.doneFlag.textContent = String(done);
315
- els.cumulativeReward.textContent = cumulativeReward.toFixed(2);
316
-
317
- const err = result.info?.validation_error ? ` error=${result.info.validation_error}` : "";
318
- appendLog(
319
- `step=${stepCount} action=${action.action_type} reward=${reward.toFixed(2)} done=${done}${err}`
320
- );
321
-
322
- setStatus(done ? "Episode completed." : "Step accepted.", done ? "ok" : "idle");
323
- } catch (err) {
324
- setStatus(`Step failed: ${err.message}`, "error");
325
- appendLog(`step_error ${err.message}`);
326
- }
327
- }
328
 
329
- async function fetchState() {
330
- try {
331
- const state = await api("/state", "GET");
332
- els.stateJson.textContent = pretty(state);
333
- if (state.initialized === false) {
334
- setStatus("Server ready. Reset to load a task.", "idle");
335
- return;
 
 
 
 
 
 
 
 
336
  }
337
- setStatus("State fetched.", "ok");
338
- } catch (err) {
339
- setStatus(`State fetch failed: ${err.message}`, "error");
 
 
 
 
 
 
 
 
340
  }
341
  }
342
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
343
  els.actionType.addEventListener("change", () => {
344
  renderPayloadFields();
345
  renderPreview();
@@ -349,10 +773,31 @@ els.payloadFields.addEventListener("input", renderPreview);
349
  els.payloadFields.addEventListener("change", renderPreview);
350
  els.resetBtn.addEventListener("click", doReset);
351
  els.stepBtn.addEventListener("click", doStep);
352
- els.stateBtn.addEventListener("click", fetchState);
 
 
 
 
 
 
353
 
 
 
 
354
  renderPayloadFields();
355
  renderPreview();
 
 
356
  setStatus("Loading server state...", "idle");
357
- fetchState();
358
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const els = {
2
  taskSelect: document.getElementById("task-select"),
3
  seedInput: document.getElementById("seed-input"),
4
  resetBtn: document.getElementById("reset-btn"),
5
  stateBtn: document.getElementById("state-btn"),
6
  stepBtn: document.getElementById("step-btn"),
7
  status: document.getElementById("status"),
8
+ healthPill: document.getElementById("health-pill"),
9
+ taskCards: document.getElementById("task-cards"),
10
+ taskBrief: document.getElementById("task-brief"),
11
  actionType: document.getElementById("action-type"),
12
+ actionHint: document.getElementById("action-hint"),
13
  payloadFields: document.getElementById("payload-fields"),
14
  actionPreview: document.getElementById("action-preview"),
15
  ticketMeta: document.getElementById("ticket-meta"),
16
+ ticketTitle: document.getElementById("ticket-title"),
17
+ ticketSummary: document.getElementById("ticket-summary"),
18
+ ticketDescription: document.getElementById("ticket-description"),
19
+ ticketComponents: document.getElementById("ticket-components"),
20
+ ticketDuplicates: document.getElementById("ticket-duplicates"),
21
+ ticketEvidence: document.getElementById("ticket-evidence"),
22
+ lastActionResult: document.getElementById("last-action-result"),
23
  ticketBody: document.getElementById("ticket-body"),
24
+ rewardBreakdown: document.getElementById("reward-breakdown"),
25
  stepLog: document.getElementById("step-log"),
26
  stateJson: document.getElementById("state-json"),
27
  stepsUsed: document.getElementById("steps-used"),
28
  stepsRemaining: document.getElementById("steps-remaining"),
29
+ queueRemaining: document.getElementById("queue-remaining"),
30
+ urgentCount: document.getElementById("urgent-count"),
31
+ slaCount: document.getElementById("sla-count"),
32
+ partialScore: document.getElementById("partial-score"),
33
  doneFlag: document.getElementById("done-flag"),
34
  cumulativeReward: document.getElementById("cumulative-reward"),
35
  };
36
 
 
 
 
 
 
37
  const DEFAULT_COMPONENTS = [
38
  "api-gateway",
39
  "auth-service",
 
56
  "data-platform",
57
  ];
58
 
59
+ const FALLBACK_TASKS = [
60
+ { id: "bug_triage_easy", difficulty: "easy", description: "8 clear tickets, minimal duplicates" },
61
+ { id: "bug_triage_medium", difficulty: "medium", description: "15 mixed-quality tickets, several duplicates" },
62
+ { id: "bug_triage_hard", difficulty: "hard", description: "25 noisy tickets, strict SLA pressure" },
63
+ ];
64
+
65
+ const TASK_GUIDE = {
66
+ bug_triage_easy: {
67
+ headline: "Clear tickets, clean decisions",
68
+ summary: "This task is about proving the environment handles practical triage basics with deterministic grading.",
69
+ points: [
70
+ "Prioritize clean severity, priority, and component labels.",
71
+ "Route valid bugs to the correct team without wasting steps.",
72
+ "Keep mistakes low and show the baseline workflow clearly.",
73
+ ],
74
+ },
75
+ bug_triage_medium: {
76
+ headline: "Noise, duplicates, and missing context",
77
+ summary: "The medium task rewards judgment instead of only raw labeling speed.",
78
+ points: [
79
+ "Catch duplicates when the hints are present.",
80
+ "Use request_info when repro data or logs are actually missing.",
81
+ "Avoid loops and destructive actions that drag the score down.",
82
+ ],
83
+ },
84
+ bug_triage_hard: {
85
+ headline: "SLA pressure and escalation discipline",
86
+ summary: "The hardest task makes queue management and critical-incident handling visible to judges.",
87
+ points: [
88
+ "Handle sev0 and sev1 reports with strong escalation behavior.",
89
+ "Protect the step budget across a larger, noisier queue.",
90
+ "Show policy quality under pressure instead of gambling on shortcuts.",
91
+ ],
92
+ },
93
+ };
94
+
95
+ const ACTION_HINTS = {
96
+ classify: "Set severity, priority, and component for the active ticket. Candidate components from the observation are shown first.",
97
+ assign: "Use this after classification to route the issue to the owning team.",
98
+ mark_duplicate: "Link the report to a canonical ticket when the duplicate hints are strong enough.",
99
+ request_info: "Ask for missing repro steps or logs when the report is incomplete.",
100
+ defer: "Use sparingly. Deferring the wrong issue can hurt both reward and grader outcomes.",
101
+ close: "Closing is high risk unless the ticket clearly deserves it.",
102
+ escalate_incident: "Escalate critical production incidents when urgency and customer impact justify it.",
103
+ next_ticket: "Move on only when you are comfortable with the current decision quality.",
104
+ };
105
+
106
+ let currentObservation = null;
107
+ let done = false;
108
+ let stepCount = 0;
109
+ let cumulativeReward = 0;
110
+ let busy = false;
111
+ let taskRegistry = { tasks: FALLBACK_TASKS };
112
 
113
  function pretty(obj) {
114
+ return JSON.stringify(obj ?? {}, null, 2);
115
  }
116
 
117
+ function escapeHtml(value) {
118
+ return String(value).replace(/[&<>"']/g, (char) => {
119
+ const table = {
120
+ "&": "&amp;",
121
+ "<": "&lt;",
122
+ ">": "&gt;",
123
+ '"': "&quot;",
124
+ "'": "&#39;",
125
+ };
126
+ return table[char] || char;
127
+ });
128
+ }
129
+
130
+ function formatDate(value) {
131
+ if (!value) {
132
+ return "unknown date";
133
+ }
134
+ const date = new Date(value);
135
+ if (Number.isNaN(date.getTime())) {
136
+ return "unknown date";
137
+ }
138
+ return new Intl.DateTimeFormat(undefined, {
139
+ year: "numeric",
140
+ month: "short",
141
+ day: "numeric",
142
+ hour: "2-digit",
143
+ minute: "2-digit",
144
+ }).format(date);
145
+ }
146
+
147
+ function formatDifficulty(value) {
148
+ if (!value) {
149
+ return "Task";
150
+ }
151
+ return value.charAt(0).toUpperCase() + value.slice(1);
152
+ }
153
+
154
+ function humanizeKey(value) {
155
+ return value
156
+ .split("_")
157
+ .map((part) => part.charAt(0).toUpperCase() + part.slice(1))
158
+ .join(" ");
159
+ }
160
+
161
+ function selectedTaskId() {
162
+ return els.taskSelect.value;
163
+ }
164
+
165
+ function availableTasks() {
166
+ return Array.isArray(taskRegistry.tasks) && taskRegistry.tasks.length
167
+ ? taskRegistry.tasks
168
+ : FALLBACK_TASKS;
169
+ }
170
+
171
+ function taskMetaById(taskId) {
172
+ return availableTasks().find((task) => task.id === taskId) || FALLBACK_TASKS[0];
173
+ }
174
+
175
+ function mergeUnique(primary, secondary) {
176
+ return [...new Set([...(primary || []), ...(secondary || [])])];
177
+ }
178
+
179
+ function currentTicket() {
180
+ return currentObservation?.current_ticket || null;
181
  }
182
 
183
  function componentsList() {
184
+ const ticket = currentTicket();
185
+ return mergeUnique(ticket?.component_candidates || [], currentObservation?.available_components || DEFAULT_COMPONENTS);
 
186
  }
187
 
188
  function teamsList() {
 
191
  : DEFAULT_TEAMS;
192
  }
193
 
194
+ function suggestedInfoType() {
195
+ const ticket = currentTicket();
196
+ if (!ticket) {
197
+ return "both";
198
+ }
199
+ if (!ticket.repro_steps_present && !ticket.logs_present) {
200
+ return "both";
201
+ }
202
+ if (!ticket.repro_steps_present) {
203
+ return "repro_steps";
204
+ }
205
+ if (!ticket.logs_present) {
206
+ return "logs";
207
+ }
208
+ return "both";
209
+ }
210
+
211
+ function setStatus(text, kind = "idle") {
212
+ els.status.textContent = text;
213
+ els.status.className = `status status-${kind}`;
214
+ }
215
+
216
+ function setHealth(text, kind = "pending") {
217
+ els.healthPill.textContent = text;
218
+ els.healthPill.className = `health-pill is-${kind}`;
219
+ }
220
+
221
+ function updateButtonState() {
222
+ const hasActiveTicket = Boolean(currentObservation?.current_ticket) && !done;
223
+ els.resetBtn.disabled = busy;
224
+ els.stateBtn.disabled = busy;
225
+ els.stepBtn.disabled = busy || !hasActiveTicket;
226
+ }
227
+
228
+ function renderTokens(container, values, emptyLabel = "None") {
229
+ container.innerHTML = "";
230
+
231
+ const items = values && values.length ? values : [{ label: emptyLabel, tone: "muted" }];
232
+ items.forEach((item) => {
233
+ const token = document.createElement("span");
234
+ const label = typeof item === "string" ? item : item.label;
235
+ const tone = typeof item === "string" ? "" : item.tone || "";
236
+ token.className = `token${tone ? ` token-${tone}` : ""}`;
237
+ token.textContent = label;
238
+ container.appendChild(token);
239
+ });
240
+ }
241
+
242
+ function renderSummaryMetrics(observation = currentObservation) {
243
+ els.stepsUsed.textContent = String(observation?.steps_used ?? stepCount ?? 0);
244
+ els.stepsRemaining.textContent = String(observation?.steps_remaining ?? 0);
245
+ els.queueRemaining.textContent = String(observation?.queue_stats?.remaining_count ?? 0);
246
+ els.urgentCount.textContent = String(observation?.queue_stats?.urgent_count ?? 0);
247
+ els.slaCount.textContent = String(observation?.queue_stats?.sla_at_risk_count ?? 0);
248
+ els.partialScore.textContent = Number(observation?.partial_score ?? 0).toFixed(2);
249
+ els.cumulativeReward.textContent = Number(cumulativeReward || 0).toFixed(2);
250
+ els.doneFlag.textContent = done ? "completed" : observation?.current_ticket ? "open" : "idle";
251
+ }
252
+
253
+ function renderTaskOptions() {
254
+ const selected = selectedTaskId();
255
+ els.taskSelect.innerHTML = availableTasks()
256
+ .map((task) => `<option value="${escapeHtml(task.id)}">${escapeHtml(task.id)}</option>`)
257
+ .join("");
258
+
259
+ els.taskSelect.value = availableTasks().some((task) => task.id === selected)
260
+ ? selected
261
+ : availableTasks()[0].id;
262
+ }
263
+
264
+ function renderTaskCards() {
265
+ const selected = selectedTaskId();
266
+ els.taskCards.innerHTML = availableTasks()
267
+ .map((task) => {
268
+ const guide = TASK_GUIDE[task.id] || TASK_GUIDE.bug_triage_easy;
269
+ const activeClass = task.id === selected ? " is-active" : "";
270
+ return `
271
+ <button type="button" class="task-card${activeClass}" data-task="${escapeHtml(task.id)}">
272
+ <small>${escapeHtml(formatDifficulty(task.difficulty))}</small>
273
+ <strong>${escapeHtml(guide.headline)}</strong>
274
+ <p>${escapeHtml(task.description)}</p>
275
+ </button>
276
+ `;
277
+ })
278
+ .join("");
279
+ }
280
+
281
+ function renderTaskBrief() {
282
+ const taskId = selectedTaskId();
283
+ const meta = taskMetaById(taskId);
284
+ const guide = TASK_GUIDE[taskId] || TASK_GUIDE.bug_triage_easy;
285
+
286
+ els.taskBrief.innerHTML = `
287
+ <div class="task-brief-header">
288
+ <h3>${escapeHtml(guide.headline)}</h3>
289
+ <span class="difficulty-pill">${escapeHtml(formatDifficulty(meta.difficulty))}</span>
290
+ </div>
291
+ <p>${escapeHtml(guide.summary)}</p>
292
+ <ul class="mini-list">
293
+ ${guide.points.map((point) => `<li>${escapeHtml(point)}</li>`).join("")}
294
+ </ul>
295
+ `;
296
+ }
297
+
298
+ function renderActionHint() {
299
+ const actionType = els.actionType.value;
300
+ const ticket = currentTicket();
301
+ let hint = ACTION_HINTS[actionType] || "Choose an action and step the environment.";
302
+
303
+ if (actionType === "mark_duplicate" && ticket?.suspected_duplicate_ids?.length) {
304
+ hint += ` Suggested canonical IDs: ${ticket.suspected_duplicate_ids.join(", ")}.`;
305
+ }
306
+ if (actionType === "request_info" && ticket) {
307
+ const missing = [];
308
+ if (!ticket.repro_steps_present) {
309
+ missing.push("repro steps");
310
+ }
311
+ if (!ticket.logs_present) {
312
+ missing.push("logs");
313
+ }
314
+ if (missing.length) {
315
+ hint += ` Missing on this ticket: ${missing.join(" and ")}.`;
316
+ }
317
+ }
318
+ if (actionType === "classify" && ticket?.component_candidates?.length) {
319
+ hint += ` Observation candidates: ${ticket.component_candidates.join(", ")}.`;
320
+ }
321
+
322
+ els.actionHint.textContent = hint;
323
+ }
324
+
325
+ function optionHtml(value, selectedValue) {
326
+ const selected = value === selectedValue ? "selected" : "";
327
+ return `<option value="${escapeHtml(value)}" ${selected}>${escapeHtml(value)}</option>`;
328
+ }
329
+
330
  function renderPayloadFields() {
331
  const actionType = els.actionType.value;
332
+ const ticket = currentTicket();
333
  const components = componentsList();
334
  const teams = teamsList();
335
+ const duplicateSuggestion = ticket?.suspected_duplicate_ids?.[0] || "";
336
 
337
  if (actionType === "classify") {
338
  els.payloadFields.innerHTML = `
 
357
  <label>
358
  <span>Component</span>
359
  <select id="f-component">
360
+ ${components.map((component, index) => optionHtml(component, index === 0 ? component : "")).join("")}
361
  </select>
362
  </label>
363
  `;
364
+ } else if (actionType === "assign") {
 
 
 
365
  els.payloadFields.innerHTML = `
366
  <label>
367
  <span>Team</span>
368
  <select id="f-team">
369
+ ${teams.map((team, index) => optionHtml(team, index === 0 ? team : "")).join("")}
370
  </select>
371
  </label>
372
  `;
373
+ } else if (actionType === "mark_duplicate") {
 
 
 
374
  els.payloadFields.innerHTML = `
375
  <label>
376
+ <span>Canonical ticket ID</span>
377
+ <input id="f-canonical" type="text" value="${escapeHtml(duplicateSuggestion)}" placeholder="BUG-1001" />
378
  </label>
379
  `;
380
+ } else if (actionType === "request_info") {
381
+ const infoType = suggestedInfoType();
 
 
382
  els.payloadFields.innerHTML = `
383
  <label>
384
+ <span>Info type</span>
385
  <select id="f-info-type">
386
+ <option value="repro_steps" ${infoType === "repro_steps" ? "selected" : ""}>repro_steps</option>
387
+ <option value="logs" ${infoType === "logs" ? "selected" : ""}>logs</option>
388
+ <option value="both" ${infoType === "both" ? "selected" : ""}>both</option>
389
  </select>
390
  </label>
391
  `;
392
+ } else if (actionType === "defer") {
 
 
 
393
  els.payloadFields.innerHTML = `
394
  <label>
395
  <span>Reason</span>
396
+ <input id="f-defer-reason" type="text" value="needs prioritization review" placeholder="Waiting for roadmap decision" />
397
  </label>
398
  `;
399
+ } else if (actionType === "close") {
 
 
 
400
  els.payloadFields.innerHTML = `
401
  <label>
402
+ <span>Close reason</span>
403
  <select id="f-close-reason">
404
  <option value="invalid">invalid</option>
405
  <option value="wont_fix">wont_fix</option>
 
408
  </select>
409
  </label>
410
  `;
411
+ } else if (actionType === "escalate_incident") {
 
 
 
412
  els.payloadFields.innerHTML = `
413
  <label>
414
  <span>Justification</span>
415
+ <textarea id="f-justification" placeholder="Production impact, customer scope, and urgency justify escalation."></textarea>
416
  </label>
417
  `;
418
+ } else {
419
+ els.payloadFields.innerHTML = `<p class="muted-copy">No payload required for this action.</p>`;
420
  }
421
 
422
+ renderActionHint();
423
  }
424
 
425
  function buildAction() {
 
464
  }
465
 
466
  function renderPreview() {
467
+ els.actionPreview.textContent = pretty(buildAction());
 
468
  }
469
 
470
+ function renderRewardBreakdown(breakdown = null) {
471
+ els.rewardBreakdown.innerHTML = "";
472
+ const entries = breakdown ? Object.entries(breakdown) : [];
473
+
474
+ if (!entries.length) {
475
+ const li = document.createElement("li");
476
+ li.textContent = "Take a step to inspect reward components.";
477
+ els.rewardBreakdown.appendChild(li);
478
  return;
479
  }
480
 
481
+ entries
482
+ .sort((a, b) => Math.abs(Number(b[1]) || 0) - Math.abs(Number(a[1]) || 0))
483
+ .forEach(([key, rawValue]) => {
484
+ const value = Number(rawValue || 0);
485
+ const li = document.createElement("li");
486
+ const label = document.createElement("span");
487
+ const score = document.createElement("strong");
488
+
489
+ label.textContent = humanizeKey(key);
490
+ score.textContent = `${value >= 0 ? "+" : ""}${value.toFixed(2)}`;
491
+ score.className = value > 0 ? "value-good" : value < 0 ? "value-bad" : "value-neutral";
492
+
493
+ li.append(label, score);
494
+ els.rewardBreakdown.appendChild(li);
495
+ });
496
  }
497
 
498
+ function appendLog({ title, meta, note, tone = "neutral" }) {
499
  const li = document.createElement("li");
500
+ li.className = `log-entry${tone === "bad" ? " is-bad" : tone === "warn" ? " is-warn" : ""}`;
501
+
502
+ const heading = document.createElement("strong");
503
+ heading.textContent = title;
504
+
505
+ const metaNode = document.createElement("div");
506
+ metaNode.className = "log-meta";
507
+ metaNode.textContent = meta;
508
+
509
+ const noteNode = document.createElement("p");
510
+ noteNode.className = "log-note";
511
+ noteNode.textContent = note;
512
+
513
+ li.append(heading, metaNode, noteNode);
514
  els.stepLog.prepend(li);
515
  }
516
 
517
+ function renderObservation(observation) {
518
+ currentObservation = observation;
519
+ const ticket = observation?.current_ticket || null;
520
+
521
+ if (!ticket) {
522
+ els.ticketMeta.textContent = done
523
+ ? "Episode completed. Reset to start another run."
524
+ : "Run reset to load a task.";
525
+ els.ticketTitle.textContent = "No active ticket";
526
+ els.ticketDescription.textContent = "The current observation does not include an active ticket.";
527
+ els.lastActionResult.textContent = observation?.last_action_result || "No action taken yet.";
528
+ renderTokens(els.ticketSummary, [], "Waiting for ticket data");
529
+ renderTokens(els.ticketComponents, [], "No component hints");
530
+ renderTokens(els.ticketDuplicates, [], "No duplicate hints");
531
+ renderTokens(els.ticketEvidence, [], "No evidence details");
532
+ els.ticketBody.textContent = pretty(observation || {});
533
+ renderSummaryMetrics(observation);
534
+ updateButtonState();
535
+ renderPayloadFields();
536
+ renderPreview();
537
+ return;
538
+ }
539
+
540
+ els.ticketMeta.textContent = `${ticket.ticket_id} · ${ticket.service} · ${formatDate(ticket.created_at)}`;
541
+ els.ticketTitle.textContent = ticket.title;
542
+ els.ticketDescription.textContent = ticket.description;
543
+ els.lastActionResult.textContent = observation?.last_action_result || "No action taken yet.";
544
+
545
+ renderTokens(els.ticketSummary, [
546
+ { label: `Reporter: ${ticket.reporter_type}` },
547
+ { label: `Tier: ${ticket.customer_tier}` },
548
+ { label: `Attachments: ${ticket.attachments_count}` },
549
+ ]);
550
+
551
+ renderTokens(
552
+ els.ticketComponents,
553
+ (ticket.component_candidates || []).map((component) => ({ label: component, tone: "good" })),
554
+ "No component hints"
555
+ );
556
+
557
+ renderTokens(
558
+ els.ticketDuplicates,
559
+ (ticket.suspected_duplicate_ids || []).map((ticketId) => ({ label: ticketId, tone: "warn" })),
560
+ "No duplicate hints"
561
+ );
562
+
563
+ renderTokens(els.ticketEvidence, [
564
+ {
565
+ label: ticket.repro_steps_present ? "Repro steps present" : "Repro steps missing",
566
+ tone: ticket.repro_steps_present ? "good" : "warn",
567
+ },
568
+ {
569
+ label: ticket.logs_present ? "Logs present" : "Logs missing",
570
+ tone: ticket.logs_present ? "good" : "warn",
571
+ },
572
+ {
573
+ label: ticket.attachments_count > 0 ? "Attachments included" : "No attachments",
574
+ tone: ticket.attachments_count > 0 ? "good" : "muted",
575
+ },
576
+ ]);
577
+
578
+ els.ticketBody.textContent = pretty(observation);
579
+ renderSummaryMetrics(observation);
580
+ renderPayloadFields();
581
+ renderPreview();
582
+ updateButtonState();
583
+ }
584
+
585
  async function api(url, method = "GET", body = null) {
586
+ const options = { method, headers: {} };
587
  if (body !== null) {
588
+ options.headers["Content-Type"] = "application/json";
589
+ options.body = JSON.stringify(body);
590
  }
591
 
592
+ const response = await fetch(url, options);
593
+ const data = await response.json().catch(() => null);
594
+ if (!response.ok) {
595
+ const message = data?.detail || `HTTP ${response.status}`;
596
+ throw new Error(message);
597
  }
598
  return data;
599
  }
600
 
601
+ async function fetchHealth() {
602
+ try {
603
+ const health = await api("/health");
604
+ setHealth(health?.status === "ok" ? "Online" : "Degraded", health?.status === "ok" ? "ok" : "bad");
605
+ } catch (error) {
606
+ setHealth("Offline", "bad");
607
+ }
608
+ }
609
+
610
+ async function fetchTasks() {
611
+ try {
612
+ const data = await api("/tasks");
613
+ if (Array.isArray(data?.tasks) && data.tasks.length) {
614
+ taskRegistry = data;
615
+ }
616
+ } catch (error) {
617
+ taskRegistry = { tasks: FALLBACK_TASKS };
618
+ }
619
+
620
+ renderTaskOptions();
621
+ renderTaskCards();
622
+ renderTaskBrief();
623
+ }
624
+
625
+ async function fetchState({ silent = false } = {}) {
626
+ const state = await api("/state");
627
+ els.stateJson.textContent = pretty(state);
628
+
629
+ if (state.initialized) {
630
+ stepCount = Number(state.steps_used || 0);
631
+ cumulativeReward = Number(state.cumulative_reward || 0);
632
+ done = Boolean(state.episode_done);
633
+
634
+ if (state.current_task_id && availableTasks().some((task) => task.id === state.current_task_id)) {
635
+ els.taskSelect.value = state.current_task_id;
636
+ renderTaskCards();
637
+ renderTaskBrief();
638
+ }
639
+
640
+ els.stepsUsed.textContent = String(state.steps_used || 0);
641
+ els.stepsRemaining.textContent = String(state.steps_remaining || 0);
642
+ els.cumulativeReward.textContent = Number(state.cumulative_reward || 0).toFixed(2);
643
+ els.doneFlag.textContent = done ? "completed" : "open";
644
+ } else if (!currentObservation) {
645
+ renderSummaryMetrics(null);
646
+ }
647
+
648
+ if (!silent) {
649
+ setStatus(state.initialized ? "State snapshot updated." : "Server ready. Reset to start an episode.", "ok");
650
+ }
651
+
652
+ updateButtonState();
653
+ return state;
654
+ }
655
+
656
  async function doReset() {
657
+ const taskId = els.taskSelect.value;
658
  const seed = Number(els.seedInput.value || 42);
659
 
660
  try {
661
+ busy = true;
662
+ updateButtonState();
663
  setStatus("Resetting episode...", "idle");
664
+
665
+ const observation = await api("/reset", "POST", { task_id: taskId, seed });
666
  done = false;
667
  stepCount = 0;
668
  cumulativeReward = 0;
669
  els.stepLog.innerHTML = "";
670
+ renderRewardBreakdown(null);
671
+ renderObservation(observation);
672
+ await fetchState({ silent: true });
673
+
674
+ appendLog({
675
+ title: `Episode reset · ${taskId}`,
676
+ meta: `seed=${seed}`,
677
+ note: taskMetaById(taskId).description,
678
+ });
679
+ setStatus(`Episode ready on ${taskId}.`, "ok");
680
+ } catch (error) {
681
+ setStatus(`Reset failed: ${error.message}`, "error");
682
+ appendLog({
683
+ title: "Reset failed",
684
+ meta: "reset",
685
+ note: error.message,
686
+ tone: "bad",
687
+ });
688
+ } finally {
689
+ busy = false;
690
+ updateButtonState();
691
  }
692
  }
693
 
694
  async function doStep() {
695
+ if (!currentObservation?.current_ticket) {
696
+ setStatus("Run reset first.", "error");
697
  return;
698
  }
699
+
700
  if (done) {
701
+ setStatus("Episode already completed. Reset to start again.", "warn");
702
  return;
703
  }
704
 
705
  const action = buildAction();
706
 
707
  try {
708
+ busy = true;
709
+ updateButtonState();
710
+ setStatus("Submitting step...", "idle");
711
+
712
  const result = await api("/step", "POST", { action });
713
  stepCount += 1;
714
  done = Boolean(result.done);
 
 
715
  cumulativeReward = Number(result.reward?.cumulative_reward || cumulativeReward);
716
 
717
  renderObservation(result.observation);
718
+ renderRewardBreakdown(result.reward?.reward_breakdown);
719
+ await fetchState({ silent: true });
 
 
 
 
 
 
 
 
 
 
 
 
720
 
721
+ const reward = Number(result.reward?.step_reward || 0);
722
+ const validationError = result.info?.validation_error;
723
+ const note = validationError || result.observation?.last_action_result || "Step accepted.";
724
+
725
+ appendLog({
726
+ title: `Step ${stepCount} · ${action.action_type}`,
727
+ meta: `reward=${reward.toFixed(2)} · ${done ? "completed" : "running"}`,
728
+ note,
729
+ tone: validationError ? "warn" : "neutral",
730
+ });
731
+
732
+ if (validationError) {
733
+ setStatus(`Step accepted with penalty: ${validationError}`, "warn");
734
+ } else {
735
+ setStatus(done ? "Episode completed." : "Step accepted.", done ? "ok" : "idle");
736
  }
737
+ } catch (error) {
738
+ setStatus(`Step failed: ${error.message}`, "error");
739
+ appendLog({
740
+ title: "Step failed",
741
+ meta: `action=${action.action_type}`,
742
+ note: error.message,
743
+ tone: "bad",
744
+ });
745
+ } finally {
746
+ busy = false;
747
+ updateButtonState();
748
  }
749
  }
750
 
751
+ els.taskSelect.addEventListener("change", () => {
752
+ renderTaskCards();
753
+ renderTaskBrief();
754
+ });
755
+
756
+ els.taskCards.addEventListener("click", (event) => {
757
+ const card = event.target.closest("[data-task]");
758
+ if (!card) {
759
+ return;
760
+ }
761
+
762
+ els.taskSelect.value = card.dataset.task;
763
+ renderTaskCards();
764
+ renderTaskBrief();
765
+ });
766
+
767
  els.actionType.addEventListener("change", () => {
768
  renderPayloadFields();
769
  renderPreview();
 
773
  els.payloadFields.addEventListener("change", renderPreview);
774
  els.resetBtn.addEventListener("click", doReset);
775
  els.stepBtn.addEventListener("click", doStep);
776
+ els.stateBtn.addEventListener("click", async () => {
777
+ try {
778
+ await fetchState();
779
+ } catch (error) {
780
+ setStatus(`State fetch failed: ${error.message}`, "error");
781
+ }
782
+ });
783
 
784
+ renderTaskOptions();
785
+ renderTaskCards();
786
+ renderTaskBrief();
787
  renderPayloadFields();
788
  renderPreview();
789
+ renderRewardBreakdown(null);
790
+ renderSummaryMetrics(null);
791
  setStatus("Loading server state...", "idle");
 
792
 
793
+ fetchHealth();
794
+ fetchTasks()
795
+ .then(() => fetchState({ silent: true }))
796
+ .then(() => {
797
+ if (!currentObservation) {
798
+ setStatus("Server ready. Reset to start an episode.", "idle");
799
+ }
800
+ })
801
+ .catch(() => {
802
+ setStatus("Server ready. Reset to start an episode.", "idle");
803
+ });
openenv_bug_triage/ui/index.html CHANGED
@@ -1,113 +1,285 @@
1
- <!doctype html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
- <title>Bug Triage OpenEnv Console</title>
7
  <link rel="icon" href="/ui/favicon.svg" type="image/svg+xml" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
- <link href="https://fonts.googleapis.com/css2?family=Space+Grotesk:wght@400;500;700&family=IBM+Plex+Mono:wght@400;500&display=swap" rel="stylesheet" />
11
  <link rel="stylesheet" href="/ui/styles.css" />
12
  </head>
13
  <body>
14
- <div class="aurora" aria-hidden="true"></div>
15
-
16
- <header class="hero reveal">
17
- <div>
18
- <p class="kicker">OpenEnv - Dark Ops Console</p>
19
- <h1>Bug Triage Environment</h1>
20
- <p class="subtitle">Interactive dark-mode frontend for reset / step / state workflows.</p>
21
- </div>
22
- <div class="pillset">
23
- <span class="pill">Real-World Task</span>
24
- <span class="pill">Typed Actions</span>
25
- <span class="pill">Dense Rewards</span>
26
- </div>
27
- </header>
28
-
29
- <main class="layout">
30
- <section class="card reveal" id="controls-card">
31
- <h2>Session Controls</h2>
32
- <div class="grid-2">
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
33
  <label>
34
- <span>Task</span>
35
- <select id="task-select">
36
- <option value="bug_triage_easy">bug_triage_easy</option>
37
- <option value="bug_triage_medium">bug_triage_medium</option>
38
- <option value="bug_triage_hard">bug_triage_hard</option>
 
 
 
 
 
39
  </select>
40
  </label>
41
- <label>
42
- <span>Seed</span>
43
- <input id="seed-input" type="number" value="42" min="0" step="1" />
44
- </label>
45
- </div>
46
- <div class="button-row">
47
- <button id="reset-btn" class="btn btn-accent">Reset Episode</button>
48
- <button id="state-btn" class="btn btn-ghost">Fetch State</button>
49
- </div>
50
- <div id="status" class="status status-idle">Idle</div>
51
- <div class="summary-grid">
52
- <article>
53
- <h3>Steps</h3>
54
- <p id="steps-used">0</p>
55
- </article>
56
- <article>
57
- <h3>Remaining</h3>
58
- <p id="steps-remaining">0</p>
59
- </article>
60
- <article>
61
- <h3>Done</h3>
62
- <p id="done-flag">false</p>
63
- </article>
64
- <article>
65
- <h3>Cumulative</h3>
66
- <p id="cumulative-reward">0.00</p>
67
- </article>
68
- </div>
69
- </section>
70
 
71
- <section class="card reveal" id="ticket-card">
72
- <h2>Current Ticket</h2>
73
- <div id="ticket-meta" class="ticket-meta">Run reset to load a task.</div>
74
- <pre id="ticket-body" class="json-view">{}</pre>
75
- </section>
76
 
77
- <section class="card reveal" id="action-card">
78
- <h2>Action Composer</h2>
79
- <label>
80
- <span>Action Type</span>
81
- <select id="action-type">
82
- <option value="classify">classify</option>
83
- <option value="assign">assign</option>
84
- <option value="mark_duplicate">mark_duplicate</option>
85
- <option value="request_info">request_info</option>
86
- <option value="defer">defer</option>
87
- <option value="close">close</option>
88
- <option value="escalate_incident">escalate_incident</option>
89
- <option value="next_ticket">next_ticket</option>
90
- </select>
91
- </label>
92
- <div id="payload-fields" class="payload-grid"></div>
93
- <div class="button-row">
94
- <button id="step-btn" class="btn btn-hot">Step</button>
95
- </div>
96
- <pre id="action-preview" class="json-view">{"action_type":"classify"}</pre>
97
- </section>
98
 
99
- <section class="card reveal" id="logs-card">
100
- <h2>Episode Log</h2>
101
- <ol id="step-log" class="log-list"></ol>
102
- </section>
 
 
 
 
103
 
104
- <section class="card reveal" id="state-card">
105
- <h2>State Snapshot</h2>
106
- <pre id="state-json" class="json-view">{}</pre>
107
- </section>
108
- </main>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
109
 
110
  <script src="/ui/app.js"></script>
111
  </body>
112
  </html>
113
-
 
1
+ <!doctype html>
2
  <html lang="en">
3
  <head>
4
  <meta charset="utf-8" />
5
  <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>OpenEnv Bug Triage Workspace</title>
7
  <link rel="icon" href="/ui/favicon.svg" type="image/svg+xml" />
8
  <link rel="preconnect" href="https://fonts.googleapis.com" />
9
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
10
+ <link href="https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;500&family=Sora:wght@400;500;600;700&display=swap" rel="stylesheet" />
11
  <link rel="stylesheet" href="/ui/styles.css" />
12
  </head>
13
  <body>
14
+ <div class="backdrop" aria-hidden="true"></div>
15
+
16
+ <div class="page-shell">
17
+ <header class="hero reveal">
18
+ <div class="hero-copy">
19
+ <p class="eyebrow">Round 1 · OpenEnv Submission</p>
20
+ <h1>Bug triage, shown as a clean real-world agent workspace.</h1>
21
+ <p class="hero-text">
22
+ This UI makes the judging story easy to follow: a real software workflow, the standard
23
+ <code>reset()</code> / <code>step()</code> / <code>state()</code> API, three graded tasks, and dense reward
24
+ feedback across the whole episode.
25
+ </p>
26
+ <div class="hero-tags">
27
+ <span class="tag">Real-world task</span>
28
+ <span class="tag">Typed models</span>
29
+ <span class="tag">3 graded tasks</span>
30
+ <span class="tag">Partial rewards</span>
31
+ </div>
32
+ </div>
33
+
34
+ <aside class="hero-aside">
35
+ <p class="aside-label">Submission fit</p>
36
+ <ul class="signal-list">
37
+ <li>Classify, route, escalate, and request missing information for bug reports.</li>
38
+ <li>See task difficulty progress from easy to hard before you ever click reset.</li>
39
+ <li>Track queue pressure, reward, and state as the episode unfolds.</li>
40
+ <li>Keep the interface simple enough for demos, judges, and quick local checks.</li>
41
+ </ul>
42
+ <div class="hero-meta">
43
+ <span>API health</span>
44
+ <strong id="health-pill" class="health-pill is-pending">Checking...</strong>
45
+ </div>
46
+ </aside>
47
+ </header>
48
+
49
+ <section class="highlight-grid reveal">
50
+ <article class="highlight-card">
51
+ <p class="card-index">01</p>
52
+ <h2>Real workflow</h2>
53
+ <p>Incoming tickets are triaged the way real software teams handle severity, ownership, duplicates, and escalations.</p>
54
+ </article>
55
+ <article class="highlight-card">
56
+ <p class="card-index">02</p>
57
+ <h2>OpenEnv ready</h2>
58
+ <p>The interface is built directly on top of the typed environment endpoints instead of a disconnected mockup.</p>
59
+ </article>
60
+ <article class="highlight-card">
61
+ <p class="card-index">03</p>
62
+ <h2>Judging visibility</h2>
63
+ <p>Task ladder, queue pressure, partial score, and reward breakdown stay visible while you step through the run.</p>
64
+ </article>
65
+ <article class="highlight-card">
66
+ <p class="card-index">04</p>
67
+ <h2>Simple demo flow</h2>
68
+ <p>Pick a task, reset the episode, compose an action, and inspect logs or raw state when you need proof.</p>
69
+ </article>
70
+ </section>
71
+
72
+ <main class="workspace" id="workspace">
73
+ <section class="panel session-panel reveal" id="controls-card">
74
+ <div class="panel-head">
75
+ <div>
76
+ <p class="section-label">Session</p>
77
+ <h2>Run an episode</h2>
78
+ </div>
79
+ <div id="status" class="status status-idle">Idle</div>
80
+ </div>
81
+
82
+ <div class="grid-2">
83
+ <label>
84
+ <span>Task</span>
85
+ <select id="task-select">
86
+ <option value="bug_triage_easy">bug_triage_easy</option>
87
+ <option value="bug_triage_medium">bug_triage_medium</option>
88
+ <option value="bug_triage_hard">bug_triage_hard</option>
89
+ </select>
90
+ </label>
91
+ <label>
92
+ <span>Seed</span>
93
+ <input id="seed-input" type="number" value="42" min="0" step="1" />
94
+ </label>
95
+ </div>
96
+
97
+ <div class="button-row">
98
+ <button id="reset-btn" class="btn btn-primary">Reset</button>
99
+ <button id="state-btn" class="btn btn-secondary">State</button>
100
+ </div>
101
+
102
+ <div class="metric-grid">
103
+ <article>
104
+ <span>Steps used</span>
105
+ <strong id="steps-used">0</strong>
106
+ </article>
107
+ <article>
108
+ <span>Remaining</span>
109
+ <strong id="steps-remaining">0</strong>
110
+ </article>
111
+ <article>
112
+ <span>Queue left</span>
113
+ <strong id="queue-remaining">0</strong>
114
+ </article>
115
+ <article>
116
+ <span>Urgent</span>
117
+ <strong id="urgent-count">0</strong>
118
+ </article>
119
+ <article>
120
+ <span>SLA risk</span>
121
+ <strong id="sla-count">0</strong>
122
+ </article>
123
+ <article>
124
+ <span>Partial score</span>
125
+ <strong id="partial-score">0.00</strong>
126
+ </article>
127
+ <article>
128
+ <span>Cumulative reward</span>
129
+ <strong id="cumulative-reward">0.00</strong>
130
+ </article>
131
+ <article>
132
+ <span>Episode</span>
133
+ <strong id="done-flag">idle</strong>
134
+ </article>
135
+ </div>
136
+ </section>
137
+
138
+ <section class="panel task-panel reveal">
139
+ <div class="panel-head">
140
+ <div>
141
+ <p class="section-label">Tasks</p>
142
+ <h2>Difficulty ladder</h2>
143
+ </div>
144
+ <p class="muted-copy">All tasks score from 0.0 to 1.0 and share the same environment API.</p>
145
+ </div>
146
+
147
+ <div id="task-cards" class="task-cards"></div>
148
+ <div id="task-brief" class="task-brief"></div>
149
+ </section>
150
+
151
+ <section class="panel ticket-panel reveal" id="ticket-card">
152
+ <div class="panel-head">
153
+ <div>
154
+ <p class="section-label">Observation</p>
155
+ <h2>Current ticket</h2>
156
+ </div>
157
+ <p id="ticket-meta" class="muted-copy">Run reset to load a task.</p>
158
+ </div>
159
+
160
+ <div class="ticket-card-shell">
161
+ <div class="ticket-top">
162
+ <h3 id="ticket-title">Waiting for a reset</h3>
163
+ <div id="ticket-summary" class="token-row"></div>
164
+ </div>
165
+
166
+ <p id="ticket-description" class="ticket-description">
167
+ Pick one of the graded tasks, reset the environment, and the focused ticket will appear here.
168
+ </p>
169
+
170
+ <div class="fact-grid">
171
+ <article class="fact-block">
172
+ <h3>Component hints</h3>
173
+ <div id="ticket-components" class="token-row"></div>
174
+ </article>
175
+ <article class="fact-block">
176
+ <h3>Duplicate hints</h3>
177
+ <div id="ticket-duplicates" class="token-row"></div>
178
+ </article>
179
+ <article class="fact-block">
180
+ <h3>Evidence</h3>
181
+ <div id="ticket-evidence" class="token-row"></div>
182
+ </article>
183
+ <article class="fact-block">
184
+ <h3>Last action result</h3>
185
+ <p id="last-action-result" class="fact-copy">No action taken yet.</p>
186
+ </article>
187
+ </div>
188
+ </div>
189
+
190
+ <details class="details-block">
191
+ <summary>Raw observation JSON</summary>
192
+ <pre id="ticket-body" class="json-view">{}</pre>
193
+ </details>
194
+ </section>
195
+
196
+ <section class="panel action-panel reveal" id="action-card">
197
+ <div class="panel-head">
198
+ <div>
199
+ <p class="section-label">Step</p>
200
+ <h2>Action builder</h2>
201
+ </div>
202
+ <p class="muted-copy">Compose a typed action and send it directly to <code>/step</code>.</p>
203
+ </div>
204
+
205
  <label>
206
+ <span>Action type</span>
207
+ <select id="action-type">
208
+ <option value="classify">classify</option>
209
+ <option value="assign">assign</option>
210
+ <option value="mark_duplicate">mark_duplicate</option>
211
+ <option value="request_info">request_info</option>
212
+ <option value="defer">defer</option>
213
+ <option value="close">close</option>
214
+ <option value="escalate_incident">escalate_incident</option>
215
+ <option value="next_ticket">next_ticket</option>
216
  </select>
217
  </label>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
 
219
+ <div id="payload-fields" class="payload-grid"></div>
220
+ <div id="action-hint" class="hint-box">Choose an action to see payload guidance.</div>
 
 
 
221
 
222
+ <div class="button-row">
223
+ <button id="step-btn" class="btn btn-accent">Submit step</button>
224
+ </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
226
+ <div class="preview-card">
227
+ <div class="preview-head">
228
+ <h3>Action preview</h3>
229
+ <span>Typed payload</span>
230
+ </div>
231
+ <pre id="action-preview" class="json-view">{"action_type":"classify"}</pre>
232
+ </div>
233
+ </section>
234
 
235
+ <section class="panel log-panel reveal" id="logs-card">
236
+ <div class="panel-head">
237
+ <div>
238
+ <p class="section-label">Trajectory</p>
239
+ <h2>Episode timeline</h2>
240
+ </div>
241
+ <p class="muted-copy">Rewards, validation feedback, and action history stay readable during demos.</p>
242
+ </div>
243
+
244
+ <div class="log-layout">
245
+ <article class="reward-card">
246
+ <h3>Last reward breakdown</h3>
247
+ <ul id="reward-breakdown" class="breakdown-list"></ul>
248
+ </article>
249
+ <ol id="step-log" class="log-list"></ol>
250
+ </div>
251
+ </section>
252
+
253
+ <section class="panel state-panel reveal" id="state-card">
254
+ <div class="panel-head">
255
+ <div>
256
+ <p class="section-label">State</p>
257
+ <h2>Environment contract</h2>
258
+ </div>
259
+ <p class="muted-copy">Use this pane to show that the environment is real, typed, and inspectable.</p>
260
+ </div>
261
+
262
+ <div class="state-layout">
263
+ <article class="api-card">
264
+ <h3>OpenEnv checklist</h3>
265
+ <ul class="mini-list">
266
+ <li>Typed observation, action, and reward models</li>
267
+ <li>Standard <code>reset()</code>, <code>step()</code>, and <code>state()</code> flow</li>
268
+ <li>Three deterministic tasks with agent graders</li>
269
+ <li>Reward signals for partial progress, not just terminal success</li>
270
+ <li>Ready for Docker and Hugging Face Spaces demos</li>
271
+ </ul>
272
+ </article>
273
+
274
+ <article class="api-card">
275
+ <h3>Live state snapshot</h3>
276
+ <pre id="state-json" class="json-view">{}</pre>
277
+ </article>
278
+ </div>
279
+ </section>
280
+ </main>
281
+ </div>
282
 
283
  <script src="/ui/app.js"></script>
284
  </body>
285
  </html>
 
openenv_bug_triage/ui/styles.css CHANGED
@@ -1,18 +1,18 @@
1
- :root {
2
- --bg: #070c14;
3
- --bg-soft: #0d1422;
4
- --surface: rgba(15, 24, 38, 0.82);
5
- --surface-strong: rgba(18, 30, 48, 0.96);
6
- --edge: rgba(132, 168, 224, 0.24);
7
- --text: #e8f0ff;
8
- --muted: #9db0cd;
9
- --accent: #25d8b4;
10
- --hot: #ff9150;
11
- --ok: #6be6a8;
12
- --warn: #ffd166;
13
- --bad: #ff5f73;
14
- --mono: "IBM Plex Mono", "Fira Code", Consolas, monospace;
15
- --sans: "Space Grotesk", "Segoe UI", sans-serif;
16
  }
17
 
18
  * {
@@ -23,102 +23,272 @@ html,
23
  body {
24
  margin: 0;
25
  min-height: 100%;
26
- background: var(--bg);
27
  color: var(--text);
28
  font-family: var(--sans);
 
 
 
 
29
  }
30
 
31
  body {
32
  position: relative;
33
  overflow-x: hidden;
34
- padding: 24px;
 
 
 
 
 
35
  }
36
 
37
- .aurora {
38
  position: fixed;
39
- inset: -25vmax;
40
  background:
41
- radial-gradient(40vmax 32vmax at 10% 16%, rgba(37, 216, 180, 0.18), transparent 70%),
42
- radial-gradient(45vmax 36vmax at 92% 8%, rgba(255, 145, 80, 0.14), transparent 72%),
43
- radial-gradient(50vmax 44vmax at 48% 105%, rgba(71, 130, 255, 0.16), transparent 74%);
44
- filter: blur(18px);
45
  pointer-events: none;
46
  z-index: -1;
47
  }
48
 
 
 
 
 
 
49
  .hero {
50
- display: flex;
51
- justify-content: space-between;
52
- align-items: flex-start;
53
- gap: 20px;
54
  margin-bottom: 18px;
55
  }
56
 
57
- .kicker {
58
- margin: 0 0 4px;
59
- letter-spacing: 0.12em;
60
- text-transform: uppercase;
61
- font-size: 0.72rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
62
  color: var(--accent);
 
 
 
63
  }
64
 
65
  h1 {
66
  margin: 0;
67
- font-size: clamp(1.5rem, 2.8vw, 2.35rem);
68
- line-height: 1.08;
 
 
69
  }
70
 
71
- .subtitle {
72
- margin-top: 8px;
 
73
  color: var(--muted);
74
- max-width: 62ch;
75
  }
76
 
77
- .pillset {
 
 
78
  display: flex;
79
  flex-wrap: wrap;
80
- gap: 8px;
 
 
 
 
81
  }
82
 
83
- .pill {
84
- border: 1px solid var(--edge);
85
- padding: 6px 10px;
 
86
  border-radius: 999px;
87
- font-size: 0.72rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  color: var(--muted);
89
- background: rgba(9, 16, 28, 0.6);
90
  }
91
 
92
- .layout {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  display: grid;
 
94
  gap: 14px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  grid-template-columns: repeat(12, minmax(0, 1fr));
 
96
  }
97
 
98
- #controls-card { grid-column: span 4; }
99
- #ticket-card { grid-column: span 8; }
100
- #action-card { grid-column: span 5; }
101
- #logs-card { grid-column: span 7; }
102
- #state-card { grid-column: span 12; }
 
103
 
104
- .card {
105
- background: linear-gradient(160deg, var(--surface), var(--surface-strong));
106
- border: 1px solid var(--edge);
107
- border-radius: 16px;
108
- padding: 16px;
109
- box-shadow: 0 18px 40px rgba(0, 0, 0, 0.26);
110
- backdrop-filter: blur(10px);
111
  }
112
 
113
- .card h2 {
114
- margin: 0 0 12px;
115
- font-size: 1rem;
 
 
 
116
  }
117
 
118
- .grid-2 {
 
 
 
 
 
 
 
 
 
 
119
  display: grid;
120
- grid-template-columns: 1fr 1fr;
121
- gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  }
123
 
124
  label {
@@ -127,8 +297,8 @@ label {
127
 
128
  label > span {
129
  display: block;
130
- margin-bottom: 6px;
131
- font-size: 0.8rem;
132
  color: var(--muted);
133
  }
134
 
@@ -143,150 +313,394 @@ input,
143
  select,
144
  textarea {
145
  width: 100%;
146
- border: 1px solid var(--edge);
147
- background: rgba(7, 13, 22, 0.88);
 
148
  color: var(--text);
149
- border-radius: 10px;
150
- padding: 10px 12px;
 
151
  }
152
 
153
- textarea {
154
- min-height: 88px;
155
- resize: vertical;
 
 
156
  }
157
 
158
- .button-row {
159
- margin-top: 12px;
160
- display: flex;
161
- gap: 10px;
162
- flex-wrap: wrap;
163
  }
164
 
165
  .btn {
166
  border: 0;
167
- border-radius: 10px;
168
  cursor: pointer;
169
- padding: 10px 14px;
170
  font-weight: 600;
171
- transition: transform 140ms ease, filter 140ms ease;
172
  }
173
 
174
- .btn:hover {
175
  transform: translateY(-1px);
176
- filter: brightness(1.06);
177
  }
178
 
179
- .btn-accent {
180
- background: linear-gradient(140deg, #22bfa8, #1ed3ba);
181
- color: #08131c;
182
  }
183
 
184
- .btn-hot {
185
- background: linear-gradient(140deg, #ff7f45, #ffa15f);
186
- color: #241208;
187
  }
188
 
189
- .btn-ghost {
190
- background: rgba(17, 27, 43, 0.85);
191
  color: var(--text);
192
- border: 1px solid var(--edge);
 
 
 
 
 
193
  }
194
 
195
  .status {
196
- margin-top: 12px;
197
- border-radius: 10px;
198
- border: 1px solid var(--edge);
199
- padding: 9px 12px;
200
- font-size: 0.9rem;
 
 
 
 
 
 
 
 
201
  }
202
 
203
  .status-ok {
204
  color: var(--ok);
205
- border-color: rgba(107, 230, 168, 0.4);
 
 
 
 
 
 
 
206
  }
207
 
208
  .status-error {
209
  color: var(--bad);
210
- border-color: rgba(255, 95, 115, 0.45);
 
211
  }
212
 
213
- .summary-grid {
214
- margin-top: 12px;
215
  display: grid;
216
- grid-template-columns: repeat(4, 1fr);
217
- gap: 8px;
218
  }
219
 
220
- .summary-grid article {
221
- border: 1px solid var(--edge);
222
- border-radius: 10px;
223
- padding: 8px;
224
- background: rgba(6, 12, 22, 0.78);
 
 
 
 
225
  }
226
 
227
- .summary-grid h3 {
228
- margin: 0;
229
- font-size: 0.7rem;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
230
  text-transform: uppercase;
231
- letter-spacing: 0.08em;
 
 
 
 
 
 
 
 
 
232
  color: var(--muted);
 
 
233
  }
234
 
235
- .summary-grid p {
236
- margin: 6px 0 0;
237
- font-family: var(--mono);
 
 
 
 
 
 
 
238
  }
239
 
240
- .ticket-meta {
 
 
 
 
 
 
 
 
 
 
 
 
241
  color: var(--muted);
242
- font-size: 0.9rem;
243
- margin-bottom: 8px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
244
  }
245
 
246
  .payload-grid {
247
  display: grid;
248
- gap: 10px;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
  }
250
 
251
  .json-view {
252
  margin: 0;
253
  white-space: pre-wrap;
254
  word-break: break-word;
255
- border-radius: 10px;
256
- border: 1px solid var(--edge);
257
- padding: 12px;
258
- background: rgba(5, 10, 18, 0.9);
 
259
  font-family: var(--mono);
260
- font-size: 0.82rem;
261
- max-height: 300px;
 
262
  overflow: auto;
263
  }
264
 
265
  .log-list {
266
  margin: 0;
267
- padding-left: 18px;
268
- max-height: 360px;
269
- overflow: auto;
 
270
  }
271
 
272
- .log-list li {
273
- margin-bottom: 8px;
274
- padding: 10px;
275
- border-radius: 10px;
276
- border: 1px solid var(--edge);
277
- background: rgba(6, 11, 18, 0.86);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
278
  font-family: var(--mono);
279
- font-size: 0.77rem;
 
 
 
 
 
 
 
 
280
  }
281
 
282
  .reveal {
283
- animation: riseIn 340ms ease both;
284
  }
285
 
286
  @keyframes riseIn {
287
  from {
288
  opacity: 0;
289
- transform: translateY(8px);
290
  }
291
  to {
292
  opacity: 1;
@@ -294,30 +708,47 @@ textarea {
294
  }
295
  }
296
 
297
- @media (max-width: 1100px) {
298
- #controls-card,
299
- #ticket-card,
300
- #action-card,
301
- #logs-card,
302
- #state-card {
303
- grid-column: span 12;
304
  }
305
 
306
- .summary-grid {
307
- grid-template-columns: repeat(2, 1fr);
 
 
 
 
 
308
  }
309
  }
310
 
311
- @media (max-width: 640px) {
312
  body {
313
- padding: 14px;
314
  }
315
 
316
- .hero {
 
 
 
 
 
 
 
 
 
 
 
317
  flex-direction: column;
 
318
  }
319
 
320
- .grid-2 {
321
- grid-template-columns: 1fr;
322
  }
323
  }
 
1
+ :root {
2
+ --bg: #f7f3ea;
3
+ --surface: rgba(255, 252, 246, 0.78);
4
+ --line: rgba(24, 56, 89, 0.12);
5
+ --text: #17304a;
6
+ --muted: #5e7388;
7
+ --accent: #0e7c86;
8
+ --warm: #ef7d47;
9
+ --ok: #2d8a57;
10
+ --warn: #a26c05;
11
+ --bad: #c05541;
12
+ --mono: "IBM Plex Mono", Consolas, monospace;
13
+ --sans: "Sora", "Segoe UI", sans-serif;
14
+ --shadow: 0 20px 60px rgba(21, 46, 72, 0.12);
15
+ --shadow-soft: 0 8px 30px rgba(21, 46, 72, 0.08);
16
  }
17
 
18
  * {
 
23
  body {
24
  margin: 0;
25
  min-height: 100%;
 
26
  color: var(--text);
27
  font-family: var(--sans);
28
+ background:
29
+ radial-gradient(circle at top left, rgba(14, 124, 134, 0.11), transparent 30%),
30
+ radial-gradient(circle at top right, rgba(239, 125, 71, 0.12), transparent 32%),
31
+ linear-gradient(180deg, #fffdf8 0%, var(--bg) 55%, #f4efe4 100%);
32
  }
33
 
34
  body {
35
  position: relative;
36
  overflow-x: hidden;
37
+ padding: 28px 22px 40px;
38
+ }
39
+
40
+ code {
41
+ font-family: var(--mono);
42
+ font-size: 0.95em;
43
  }
44
 
45
+ .backdrop {
46
  position: fixed;
47
+ inset: -10vmax;
48
  background:
49
+ radial-gradient(42vmax 34vmax at 8% 8%, rgba(14, 124, 134, 0.13), transparent 70%),
50
+ radial-gradient(38vmax 34vmax at 94% 12%, rgba(239, 125, 71, 0.11), transparent 72%),
51
+ radial-gradient(44vmax 36vmax at 50% 100%, rgba(45, 138, 87, 0.08), transparent 70%);
52
+ filter: blur(10px);
53
  pointer-events: none;
54
  z-index: -1;
55
  }
56
 
57
+ .page-shell {
58
+ width: min(1380px, 100%);
59
+ margin: 0 auto;
60
+ }
61
+
62
  .hero {
63
+ display: grid;
64
+ grid-template-columns: minmax(0, 1.5fr) minmax(280px, 0.9fr);
65
+ gap: 18px;
66
+ align-items: stretch;
67
  margin-bottom: 18px;
68
  }
69
 
70
+ .hero-copy,
71
+ .hero-aside,
72
+ .highlight-card,
73
+ .panel {
74
+ border: 1px solid var(--line);
75
+ background: var(--surface);
76
+ backdrop-filter: blur(14px);
77
+ box-shadow: var(--shadow-soft);
78
+ }
79
+
80
+ .hero-copy,
81
+ .hero-aside {
82
+ border-radius: 28px;
83
+ padding: 24px;
84
+ }
85
+
86
+ .eyebrow,
87
+ .aside-label,
88
+ .section-label,
89
+ .card-index {
90
+ margin: 0 0 10px;
91
  color: var(--accent);
92
+ font-size: 0.76rem;
93
+ letter-spacing: 0.14em;
94
+ text-transform: uppercase;
95
  }
96
 
97
  h1 {
98
  margin: 0;
99
+ max-width: 12ch;
100
+ font-size: clamp(2.15rem, 4vw, 4rem);
101
+ line-height: 1.02;
102
+ letter-spacing: -0.04em;
103
  }
104
 
105
+ .hero-text {
106
+ max-width: 66ch;
107
+ margin: 14px 0 0;
108
  color: var(--muted);
109
+ line-height: 1.7;
110
  }
111
 
112
+ .hero-tags,
113
+ .token-row,
114
+ .button-row {
115
  display: flex;
116
  flex-wrap: wrap;
117
+ gap: 10px;
118
+ }
119
+
120
+ .hero-tags {
121
+ margin-top: 18px;
122
  }
123
 
124
+ .tag,
125
+ .token {
126
+ display: inline-flex;
127
+ align-items: center;
128
  border-radius: 999px;
129
+ padding: 8px 12px;
130
+ border: 1px solid var(--line);
131
+ background: rgba(255, 255, 255, 0.7);
132
+ color: var(--text);
133
+ font-size: 0.82rem;
134
+ }
135
+
136
+ .token-good {
137
+ background: rgba(45, 138, 87, 0.12);
138
+ color: var(--ok);
139
+ border-color: rgba(45, 138, 87, 0.22);
140
+ }
141
+
142
+ .token-warn {
143
+ background: rgba(162, 108, 5, 0.12);
144
+ color: var(--warn);
145
+ border-color: rgba(162, 108, 5, 0.2);
146
+ }
147
+
148
+ .token-bad {
149
+ background: rgba(192, 85, 65, 0.12);
150
+ color: var(--bad);
151
+ border-color: rgba(192, 85, 65, 0.22);
152
+ }
153
+
154
+ .token-muted {
155
+ color: var(--muted);
156
+ background: rgba(246, 241, 232, 0.86);
157
+ }
158
+
159
+ .signal-list,
160
+ .mini-list,
161
+ .breakdown-list {
162
+ margin: 0;
163
+ padding-left: 18px;
164
+ }
165
+
166
+ .signal-list li,
167
+ .mini-list li,
168
+ .breakdown-list li {
169
+ margin-bottom: 8px;
170
+ line-height: 1.55;
171
+ }
172
+
173
+ .hero-meta {
174
+ margin-top: 18px;
175
+ padding-top: 18px;
176
+ border-top: 1px solid var(--line);
177
+ display: flex;
178
+ align-items: center;
179
+ justify-content: space-between;
180
+ gap: 12px;
181
  color: var(--muted);
 
182
  }
183
 
184
+ .health-pill {
185
+ display: inline-flex;
186
+ align-items: center;
187
+ justify-content: center;
188
+ min-width: 100px;
189
+ border-radius: 999px;
190
+ padding: 8px 12px;
191
+ font-size: 0.82rem;
192
+ border: 1px solid var(--line);
193
+ background: rgba(255, 255, 255, 0.8);
194
+ }
195
+
196
+ .health-pill.is-ok {
197
+ color: var(--ok);
198
+ border-color: rgba(45, 138, 87, 0.22);
199
+ background: rgba(45, 138, 87, 0.1);
200
+ }
201
+
202
+ .health-pill.is-bad {
203
+ color: var(--bad);
204
+ border-color: rgba(192, 85, 65, 0.22);
205
+ background: rgba(192, 85, 65, 0.1);
206
+ }
207
+
208
+ .health-pill.is-pending {
209
+ color: var(--warn);
210
+ border-color: rgba(162, 108, 5, 0.22);
211
+ background: rgba(162, 108, 5, 0.08);
212
+ }
213
+
214
+ .highlight-grid {
215
  display: grid;
216
+ grid-template-columns: repeat(4, minmax(0, 1fr));
217
  gap: 14px;
218
+ margin-bottom: 18px;
219
+ }
220
+
221
+ .highlight-card {
222
+ border-radius: 24px;
223
+ padding: 20px;
224
+ }
225
+
226
+ .highlight-card h2,
227
+ .panel h2 {
228
+ margin: 0;
229
+ font-size: 1.08rem;
230
+ }
231
+
232
+ .highlight-card p:last-child {
233
+ margin: 10px 0 0;
234
+ color: var(--muted);
235
+ line-height: 1.6;
236
+ }
237
+
238
+ .workspace {
239
+ display: grid;
240
  grid-template-columns: repeat(12, minmax(0, 1fr));
241
+ gap: 14px;
242
  }
243
 
244
+ .session-panel { grid-column: span 4; }
245
+ .task-panel { grid-column: span 8; }
246
+ .ticket-panel { grid-column: span 7; }
247
+ .action-panel { grid-column: span 5; }
248
+ .log-panel { grid-column: span 7; }
249
+ .state-panel { grid-column: span 5; }
250
 
251
+ .panel {
252
+ border-radius: 24px;
253
+ padding: 20px;
254
+ box-shadow: var(--shadow);
 
 
 
255
  }
256
 
257
+ .panel-head {
258
+ display: flex;
259
+ align-items: flex-start;
260
+ justify-content: space-between;
261
+ gap: 12px;
262
+ margin-bottom: 16px;
263
  }
264
 
265
+ .muted-copy {
266
+ margin: 0;
267
+ color: var(--muted);
268
+ line-height: 1.55;
269
+ max-width: 38ch;
270
+ }
271
+
272
+ .grid-2,
273
+ .fact-grid,
274
+ .state-layout,
275
+ .log-layout {
276
  display: grid;
277
+ gap: 12px;
278
+ }
279
+
280
+ .grid-2 {
281
+ grid-template-columns: repeat(2, minmax(0, 1fr));
282
+ }
283
+
284
+ .fact-grid {
285
+ grid-template-columns: repeat(2, minmax(0, 1fr));
286
+ margin-top: 18px;
287
+ }
288
+
289
+ .state-layout,
290
+ .log-layout {
291
+ grid-template-columns: minmax(0, 1fr);
292
  }
293
 
294
  label {
 
297
 
298
  label > span {
299
  display: block;
300
+ margin-bottom: 8px;
301
+ font-size: 0.82rem;
302
  color: var(--muted);
303
  }
304
 
 
313
  select,
314
  textarea {
315
  width: 100%;
316
+ border-radius: 16px;
317
+ border: 1px solid var(--line);
318
+ background: rgba(255, 255, 255, 0.82);
319
  color: var(--text);
320
+ padding: 12px 14px;
321
+ outline: none;
322
+ transition: border-color 160ms ease, box-shadow 160ms ease;
323
  }
324
 
325
+ input:focus,
326
+ select:focus,
327
+ textarea:focus {
328
+ border-color: rgba(14, 124, 134, 0.4);
329
+ box-shadow: 0 0 0 4px rgba(14, 124, 134, 0.1);
330
  }
331
 
332
+ textarea {
333
+ min-height: 104px;
334
+ resize: vertical;
 
 
335
  }
336
 
337
  .btn {
338
  border: 0;
339
+ border-radius: 999px;
340
  cursor: pointer;
341
+ padding: 11px 18px;
342
  font-weight: 600;
343
+ transition: transform 160ms ease, filter 160ms ease, opacity 160ms ease;
344
  }
345
 
346
+ .btn:hover:not(:disabled) {
347
  transform: translateY(-1px);
348
+ filter: brightness(1.03);
349
  }
350
 
351
+ .btn:disabled {
352
+ cursor: not-allowed;
353
+ opacity: 0.6;
354
  }
355
 
356
+ .btn-primary {
357
+ background: linear-gradient(135deg, var(--accent), #1f96a1);
358
+ color: #f7fffe;
359
  }
360
 
361
+ .btn-secondary {
362
+ background: rgba(255, 255, 255, 0.82);
363
  color: var(--text);
364
+ border: 1px solid var(--line);
365
+ }
366
+
367
+ .btn-accent {
368
+ background: linear-gradient(135deg, var(--warm), #f49d61);
369
+ color: #fff9f5;
370
  }
371
 
372
  .status {
373
+ min-height: 42px;
374
+ display: inline-flex;
375
+ align-items: center;
376
+ justify-content: center;
377
+ border-radius: 999px;
378
+ border: 1px solid var(--line);
379
+ padding: 8px 14px;
380
+ font-size: 0.85rem;
381
+ background: rgba(255, 255, 255, 0.8);
382
+ }
383
+
384
+ .status-idle {
385
+ color: var(--muted);
386
  }
387
 
388
  .status-ok {
389
  color: var(--ok);
390
+ border-color: rgba(45, 138, 87, 0.2);
391
+ background: rgba(45, 138, 87, 0.08);
392
+ }
393
+
394
+ .status-warn {
395
+ color: var(--warn);
396
+ border-color: rgba(162, 108, 5, 0.2);
397
+ background: rgba(162, 108, 5, 0.08);
398
  }
399
 
400
  .status-error {
401
  color: var(--bad);
402
+ border-color: rgba(192, 85, 65, 0.2);
403
+ background: rgba(192, 85, 65, 0.08);
404
  }
405
 
406
+ .metric-grid {
407
+ margin-top: 18px;
408
  display: grid;
409
+ grid-template-columns: repeat(2, minmax(0, 1fr));
410
+ gap: 10px;
411
  }
412
 
413
+ .metric-grid article,
414
+ .fact-block,
415
+ .reward-card,
416
+ .api-card,
417
+ .preview-card,
418
+ .task-brief {
419
+ border: 1px solid var(--line);
420
+ border-radius: 18px;
421
+ background: rgba(255, 255, 255, 0.72);
422
  }
423
 
424
+ .metric-grid article {
425
+ padding: 14px;
426
+ }
427
+
428
+ .metric-grid span {
429
+ display: block;
430
+ color: var(--muted);
431
+ font-size: 0.78rem;
432
+ margin-bottom: 8px;
433
+ }
434
+
435
+ .metric-grid strong {
436
+ display: block;
437
+ font-size: 1.24rem;
438
+ letter-spacing: -0.03em;
439
+ }
440
+
441
+ .task-cards {
442
+ display: grid;
443
+ grid-template-columns: repeat(3, minmax(0, 1fr));
444
+ gap: 10px;
445
+ }
446
+
447
+ .task-card {
448
+ width: 100%;
449
+ text-align: left;
450
+ border-radius: 20px;
451
+ border: 1px solid var(--line);
452
+ background: rgba(255, 255, 255, 0.7);
453
+ padding: 16px;
454
+ color: var(--text);
455
+ }
456
+
457
+ .task-card.is-active {
458
+ border-color: rgba(14, 124, 134, 0.35);
459
+ background: linear-gradient(180deg, rgba(14, 124, 134, 0.08), rgba(255, 255, 255, 0.85));
460
+ box-shadow: inset 0 0 0 1px rgba(14, 124, 134, 0.1);
461
+ }
462
+
463
+ .task-card small {
464
+ display: inline-flex;
465
+ margin-bottom: 10px;
466
+ color: var(--accent);
467
+ letter-spacing: 0.12em;
468
  text-transform: uppercase;
469
+ }
470
+
471
+ .task-card strong {
472
+ display: block;
473
+ font-size: 1rem;
474
+ margin-bottom: 8px;
475
+ }
476
+
477
+ .task-card p {
478
+ margin: 0;
479
  color: var(--muted);
480
+ line-height: 1.55;
481
+ font-size: 0.88rem;
482
  }
483
 
484
+ .task-brief {
485
+ margin-top: 12px;
486
+ padding: 18px;
487
+ }
488
+
489
+ .task-brief-header {
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: space-between;
493
+ gap: 12px;
494
  }
495
 
496
+ .difficulty-pill {
497
+ display: inline-flex;
498
+ align-items: center;
499
+ border-radius: 999px;
500
+ padding: 7px 12px;
501
+ background: rgba(14, 124, 134, 0.08);
502
+ color: var(--accent);
503
+ font-size: 0.8rem;
504
+ border: 1px solid rgba(14, 124, 134, 0.14);
505
+ }
506
+
507
+ .task-brief p {
508
+ margin: 10px 0 14px;
509
  color: var(--muted);
510
+ line-height: 1.6;
511
+ }
512
+
513
+ .ticket-card-shell {
514
+ border: 1px solid var(--line);
515
+ border-radius: 20px;
516
+ padding: 18px;
517
+ background: linear-gradient(180deg, rgba(255, 255, 255, 0.84), rgba(250, 245, 237, 0.9));
518
+ }
519
+
520
+ .ticket-top h3,
521
+ .preview-head h3,
522
+ .reward-card h3,
523
+ .api-card h3 {
524
+ margin: 0;
525
+ font-size: 1rem;
526
+ }
527
+
528
+ .ticket-description,
529
+ .fact-copy {
530
+ margin: 12px 0 0;
531
+ color: var(--muted);
532
+ line-height: 1.65;
533
+ }
534
+
535
+ .fact-block {
536
+ padding: 14px;
537
+ }
538
+
539
+ .fact-block h3 {
540
+ margin: 0 0 10px;
541
+ font-size: 0.85rem;
542
+ }
543
+
544
+ .details-block {
545
+ margin-top: 14px;
546
+ border-radius: 18px;
547
+ border: 1px solid var(--line);
548
+ background: rgba(255, 255, 255, 0.7);
549
+ overflow: hidden;
550
+ }
551
+
552
+ .details-block summary {
553
+ cursor: pointer;
554
+ list-style: none;
555
+ padding: 14px 16px;
556
+ font-weight: 600;
557
+ }
558
+
559
+ .details-block summary::-webkit-details-marker {
560
+ display: none;
561
  }
562
 
563
  .payload-grid {
564
  display: grid;
565
+ gap: 12px;
566
+ margin-top: 14px;
567
+ }
568
+
569
+ .hint-box {
570
+ margin-top: 14px;
571
+ border-radius: 18px;
572
+ padding: 14px 16px;
573
+ border: 1px dashed rgba(14, 124, 134, 0.22);
574
+ background: rgba(14, 124, 134, 0.06);
575
+ color: var(--muted);
576
+ line-height: 1.6;
577
+ }
578
+
579
+ .preview-card,
580
+ .reward-card,
581
+ .api-card {
582
+ padding: 16px;
583
+ }
584
+
585
+ .preview-card {
586
+ margin-top: 14px;
587
+ }
588
+
589
+ .preview-head {
590
+ display: flex;
591
+ align-items: center;
592
+ justify-content: space-between;
593
+ gap: 12px;
594
+ margin-bottom: 10px;
595
+ }
596
+
597
+ .preview-head span {
598
+ color: var(--muted);
599
+ font-size: 0.8rem;
600
+ }
601
+
602
+ .breakdown-list {
603
+ margin-top: 12px;
604
+ }
605
+
606
+ .breakdown-list li {
607
+ display: flex;
608
+ align-items: center;
609
+ justify-content: space-between;
610
+ gap: 12px;
611
+ padding: 10px 0;
612
+ border-bottom: 1px solid rgba(24, 56, 89, 0.08);
613
+ }
614
+
615
+ .breakdown-list li:last-child {
616
+ border-bottom: 0;
617
+ padding-bottom: 0;
618
+ }
619
+
620
+ .breakdown-list strong {
621
+ font-family: var(--mono);
622
+ font-size: 0.84rem;
623
+ }
624
+
625
+ .value-good {
626
+ color: var(--ok);
627
+ }
628
+
629
+ .value-bad {
630
+ color: var(--bad);
631
+ }
632
+
633
+ .value-neutral {
634
+ color: var(--muted);
635
  }
636
 
637
  .json-view {
638
  margin: 0;
639
  white-space: pre-wrap;
640
  word-break: break-word;
641
+ border-radius: 18px;
642
+ border: 1px solid var(--line);
643
+ padding: 14px;
644
+ background: rgba(248, 243, 234, 0.95);
645
+ color: #1f3850;
646
  font-family: var(--mono);
647
+ font-size: 0.8rem;
648
+ line-height: 1.65;
649
+ max-height: 360px;
650
  overflow: auto;
651
  }
652
 
653
  .log-list {
654
  margin: 0;
655
+ padding-left: 0;
656
+ list-style: none;
657
+ display: grid;
658
+ gap: 10px;
659
  }
660
 
661
+ .log-entry {
662
+ border-radius: 18px;
663
+ border: 1px solid var(--line);
664
+ background: rgba(255, 255, 255, 0.76);
665
+ padding: 14px;
666
+ }
667
+
668
+ .log-entry.is-bad {
669
+ border-color: rgba(192, 85, 65, 0.24);
670
+ background: rgba(192, 85, 65, 0.08);
671
+ }
672
+
673
+ .log-entry.is-warn {
674
+ border-color: rgba(162, 108, 5, 0.24);
675
+ background: rgba(162, 108, 5, 0.08);
676
+ }
677
+
678
+ .log-head strong {
679
+ font-size: 0.94rem;
680
+ }
681
+
682
+ .log-meta {
683
+ color: var(--muted);
684
  font-family: var(--mono);
685
+ font-size: 0.76rem;
686
+ margin-top: 8px;
687
+ }
688
+
689
+ .log-note {
690
+ color: var(--muted);
691
+ margin-top: 8px;
692
+ line-height: 1.55;
693
+ font-size: 0.86rem;
694
  }
695
 
696
  .reveal {
697
+ animation: riseIn 360ms ease both;
698
  }
699
 
700
  @keyframes riseIn {
701
  from {
702
  opacity: 0;
703
+ transform: translateY(10px);
704
  }
705
  to {
706
  opacity: 1;
 
708
  }
709
  }
710
 
711
+ @media (max-width: 1200px) {
712
+ .hero {
713
+ grid-template-columns: minmax(0, 1fr);
714
+ }
715
+
716
+ .highlight-grid {
717
+ grid-template-columns: repeat(2, minmax(0, 1fr));
718
  }
719
 
720
+ .session-panel,
721
+ .task-panel,
722
+ .ticket-panel,
723
+ .action-panel,
724
+ .log-panel,
725
+ .state-panel {
726
+ grid-column: span 12;
727
  }
728
  }
729
 
730
+ @media (max-width: 760px) {
731
  body {
732
+ padding: 16px 14px 28px;
733
  }
734
 
735
+ .highlight-grid,
736
+ .task-cards,
737
+ .grid-2,
738
+ .fact-grid,
739
+ .metric-grid {
740
+ grid-template-columns: minmax(0, 1fr);
741
+ }
742
+
743
+ .panel-head,
744
+ .hero-meta,
745
+ .preview-head,
746
+ .task-brief-header {
747
  flex-direction: column;
748
+ align-items: flex-start;
749
  }
750
 
751
+ h1 {
752
+ max-width: none;
753
  }
754
  }