File size: 10,649 Bytes
ea373a2
b92d20c
 
 
 
 
 
 
ea373a2
b92d20c
 
 
 
 
 
 
 
ea373a2
b92d20c
 
 
 
 
 
 
 
ea373a2
b92d20c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea373a2
b92d20c
ea373a2
 
 
 
 
 
 
 
 
 
b92d20c
 
 
 
 
 
ea373a2
b92d20c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
ea373a2
b92d20c
 
 
 
ea373a2
 
 
 
 
 
 
b92d20c
 
 
 
 
 
 
 
ea373a2
 
 
 
 
 
 
b92d20c
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
const API_BASE = "";

let sessionId = null;
let currentObservation = null;
let selectedArtifactId = null;

const presets = {
  pagination: {
    task_id: "pagination-regression",
    title: "Still missing page validation",
    file: "utils/pagination.py",
    severity: "medium",
    line: "start = (page - 1) * page_size",
    rationale: "The off-by-one fix is correct, but page 0 or negative values still produce negative slicing from the end of the list and return the wrong results.",
    recommendation: "Validate page >= 1 before computing slice boundaries and raise ValueError for invalid page numbers.",
  },
  auth: {
    task_id: "tenant-export-auth",
    title: "Export route is missing authz guards",
    file: "api/admin_exports.py",
    severity: "critical",
    line: "export_invoices",
    rationale: "The handler trusts account_id from the query string and never calls require_admin or require_account_scope, so another tenant's invoices can be exported.",
    recommendation: "Call require_admin and require_account_scope before reading account_id or invoking invoice_repo.export_csv.",
  },
  refund: {
    task_id: "refund-idempotency",
    title: "Retry path can send duplicate refunds",
    file: "workers/refunds.py",
    severity: "critical",
    line: "process_refund",
    rationale: "The timeout branch calls the payment processor again without a persisted idempotency key, so concurrent workers or timeout-after-success cases can issue a second refund.",
    recommendation: "Persist an idempotency_key on the refund row, reuse it on every retry, and claim the job before calling the processor.",
  },
};

document.addEventListener("DOMContentLoaded", () => {
  document.getElementById("btn-load-new").addEventListener("click", () => loadEpisode());
  document.getElementById("btn-submit").addEventListener("click", submitFinding);
  document.querySelectorAll("[data-preset]").forEach((button) => {
    button.addEventListener("click", () => applyPreset(button.dataset.preset));
  });

  checkHealth();
  loadEpisode();
});

async function checkHealth() {
  try {
    const response = await fetch(`${API_BASE}/health`);
    const data = await response.json();
    const dot = document.getElementById("status-dot");
    const text = document.getElementById("status-text");
    if (data.status === "ok") {
      dot.classList.add("status-dot--live");
      text.textContent = "Live";
    } else {
      text.textContent = "Degraded";
    }
  } catch (error) {
    document.getElementById("status-text").textContent = "Offline";
  }
}

async function loadEpisode(overrideTaskId = null) {
  try {
    let taskId = overrideTaskId;
    if (!taskId) {
       const selector = document.getElementById("task-selector");
       if (selector) taskId = selector.value;
    }
    const response = await fetch(`${API_BASE}/reset`, { 
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(taskId ? { task_id: taskId } : {})
    });
    const data = await response.json();
    sessionId = data.session_id;
    currentObservation = data.observation;
    selectedArtifactId = currentObservation.opened_artifacts[0]?.artifact_id || currentObservation.available_artifacts[0]?.artifact_id || null;
    renderObservation(currentObservation);
    resetResultPanels();
    clearForm();
  } catch (error) {
    document.getElementById("task-title").textContent = "Failed to load task";
    document.getElementById("task-summary").textContent = "The backend did not respond to /reset.";
  }
}

function renderObservation(observation) {
  document.getElementById("task-title").textContent = observation.title;
  document.getElementById("task-id").textContent = observation.task_id;
  document.getElementById("task-difficulty").textContent = observation.difficulty;
  document.getElementById("task-step-limit").textContent = `${observation.step_limit} steps`;
  document.getElementById("task-summary").textContent = observation.summary;
  document.getElementById("steps-value").textContent = observation.metadata?.step_count ?? 0;
  document.getElementById("done-value").textContent = observation.done ? "Done" : "Active";
  document.getElementById("score-value").textContent = Number(observation.score || 0).toFixed(2);

  renderArtifacts(observation.available_artifacts || []);
  renderArtifactContent(observation, selectedArtifactId);
  renderList("events-list", observation.recent_events || [], "No events yet.");
}

function renderArtifacts(artifacts) {
  const container = document.getElementById("artifact-list");
  container.innerHTML = "";
  artifacts.forEach((artifact) => {
    const button = document.createElement("button");
    button.className = `artifact-chip${artifact.artifact_id === selectedArtifactId ? " artifact-chip--active" : ""}`;
    button.textContent = `${artifact.artifact_id}${artifact.kind}`;
    button.addEventListener("click", async () => {
      selectedArtifactId = artifact.artifact_id;
      if (!artifact.opened) {
        await openArtifact(artifact.artifact_id);
      } else {
        renderObservation(currentObservation);
      }
    });
    container.appendChild(button);
  });
}

function renderArtifactContent(observation, artifactId) {
  const artifact = (observation.available_artifacts || []).find((item) => item.artifact_id === artifactId)
    || observation.opened_artifacts?.[0]
    || observation.available_artifacts?.[0];
  if (!artifact) {
    document.getElementById("artifact-content").textContent = "No artifact available.";
    return;
  }
  const content = artifact.opened ? artifact.content : `${artifact.preview}\n\nClick the artifact to open full content.`;
  document.getElementById("artifact-content").textContent = content || artifact.preview || "No content available.";
}

async function openArtifact(artifactId) {
  const response = await fetch(`${API_BASE}/step`, {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      session_id: sessionId,
      action: {
        action_type: "open_artifact",
        artifact_id: artifactId,
      },
    }),
  });
  const data = await response.json();
  currentObservation = data.observation;
  renderObservation(currentObservation);
  updateReward(data.reward || 0, currentObservation.score || 0, currentObservation.done);
  renderBreakdown(currentObservation.metadata || {});
}

async function submitFinding() {
  const finding = buildFinding();
  if (!finding) {
    return;
  }

  const button = document.getElementById("btn-submit");
  button.disabled = true;
  button.textContent = "Submitting...";

  try {
    const response = await fetch(`${API_BASE}/step`, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({
        session_id: sessionId,
        action: {
          action_type: "submit_review",
          findings: [finding],
        },
      }),
    });
    const data = await response.json();
    currentObservation = data.observation;
    renderObservation(currentObservation);
    updateReward(data.reward || 0, currentObservation.score || 0, currentObservation.done);
    renderBreakdown(currentObservation.metadata || {});
  } catch (error) {
    alert("Submission failed. Check the backend logs.");
  } finally {
    button.disabled = false;
    button.textContent = "Submit Review";
  }
}

function buildFinding() {
  const title = document.getElementById("finding-title").value.trim();
  const filePath = document.getElementById("finding-file").value.trim();
  const lineHint = document.getElementById("finding-line").value.trim();
  const severity = document.getElementById("finding-severity").value;
  const rationale = document.getElementById("finding-rationale").value.trim();
  const recommendation = document.getElementById("finding-recommendation").value.trim();

  if (!title || !filePath || !rationale || !recommendation) {
    alert("Fill title, file path, rationale, and recommendation before submitting.");
    return null;
  }

  return {
    title,
    file_path: filePath,
    line_hint: lineHint || null,
    severity,
    rationale,
    recommendation,
  };
}

async function applyPreset(name) {
  const preset = presets[name];
  if (!preset) {
    return;
  }
  
  if (!currentObservation || currentObservation.task_id !== preset.task_id) {
    const selector = document.getElementById("task-selector");
    if (selector) selector.value = preset.task_id;
    await loadEpisode(preset.task_id);
  }

  document.getElementById("finding-title").value = preset.title;
  document.getElementById("finding-file").value = preset.file;
  document.getElementById("finding-severity").value = preset.severity;
  document.getElementById("finding-line").value = preset.line;
  document.getElementById("finding-rationale").value = preset.rationale;
  document.getElementById("finding-recommendation").value = preset.recommendation;
}

function clearForm() {
  document.getElementById("finding-title").value = "";
  document.getElementById("finding-file").value = "";
  document.getElementById("finding-severity").value = "medium";
  document.getElementById("finding-line").value = "";
  document.getElementById("finding-rationale").value = "";
  document.getElementById("finding-recommendation").value = "";
}

function updateReward(reward, score, done) {
  document.getElementById("reward-value").textContent = Number(reward).toFixed(2);
  document.getElementById("score-value").textContent = Number(score).toFixed(2);
  document.getElementById("done-value").textContent = done ? "Done" : "Active";
}

function renderBreakdown(metadata) {
  const components = metadata.reward_breakdown || {};
  const graderDetails = metadata.grader_details || [];
  renderList(
    "criteria-list",
    graderDetails.map((detail) => `${detail.criterion_id}: ${detail.score}`),
    "No graded criteria yet."
  );
  renderList(
    "components-list",
    Object.entries(components).map(([key, value]) => `${key}: ${value}`),
    "No reward components yet."
  );
}

function renderList(elementId, items, emptyText) {
  const list = document.getElementById(elementId);
  list.innerHTML = "";
  if (!items.length) {
    const item = document.createElement("li");
    item.textContent = emptyText;
    list.appendChild(item);
    return;
  }
  items.forEach((text) => {
    const item = document.createElement("li");
    item.textContent = text;
    list.appendChild(item);
  });
}

function resetResultPanels() {
  updateReward(0, 0, false);
  renderList("criteria-list", [], "No graded criteria yet.");
  renderList("components-list", [], "No reward components yet.");
}