3v324v23 commited on
Commit
eabc9c6
·
1 Parent(s): 55261e4

FIX: gitignore for Next.js, add lib/api.js and HTML reference

Browse files
.gitignore CHANGED
@@ -8,14 +8,14 @@ __pycache__/
8
 
9
  # Distribution / packaging
10
  .Python
11
- build/
12
  develop-eggs/
13
  dist/
14
  downloads/
15
  eggs/
16
  .eggs/
17
- lib/
18
- lib64/
19
  parts/
20
  sdist/
21
  var/
@@ -147,6 +147,10 @@ cython_debug/
147
  .DS_Store
148
  Thumbs.db
149
 
 
 
 
 
150
  # Project-specific artifacts
151
  *.png
152
  temp_*
 
8
 
9
  # Distribution / packaging
10
  .Python
11
+ /build/
12
  develop-eggs/
13
  dist/
14
  downloads/
15
  eggs/
16
  .eggs/
17
+ /lib/
18
+ /lib64/
19
  parts/
20
  sdist/
21
  var/
 
147
  .DS_Store
148
  Thumbs.db
149
 
150
+ # Next.js
151
+ node_modules/
152
+ .next/
153
+
154
  # Project-specific artifacts
155
  *.png
156
  temp_*
pr_review_dashboard.html ADDED
@@ -0,0 +1,287 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+
2
+ <style>
3
+ *{box-sizing:border-box;margin:0;padding:0}
4
+ body{font-family:var(--font-sans)}
5
+ .dash{display:grid;grid-template-columns:220px 1fr;min-height:600px;gap:0;border:0.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg);overflow:hidden;background:var(--color-background-primary)}
6
+ .sidebar{background:var(--color-background-secondary);border-right:0.5px solid var(--color-border-tertiary);padding:16px;display:flex;flex-direction:column;gap:14px}
7
+ .sidebar-label{font-size:11px;font-weight:500;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.06em;margin-bottom:4px}
8
+ .sidebar select,.sidebar input{width:100%;font-size:13px;padding:7px 10px;background:var(--color-background-primary);border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-md);color:var(--color-text-primary)}
9
+ .init-btn{width:100%;padding:9px;font-size:13px;font-weight:500;background:var(--color-background-primary);border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-md);color:var(--color-text-primary);cursor:pointer;transition:background .15s}
10
+ .init-btn:hover{background:var(--color-background-tertiary)}
11
+ .init-btn.active{background:#1a7f3c;color:#fff;border-color:#1a7f3c}
12
+ .main{display:flex;flex-direction:column;gap:0;overflow:hidden}
13
+ .topbar{padding:14px 18px;border-bottom:0.5px solid var(--color-border-tertiary);display:flex;align-items:center;justify-content:space-between}
14
+ .pr-title{font-size:15px;font-weight:500;color:var(--color-text-primary)}
15
+ .pr-sub{font-size:12px;color:var(--color-text-secondary);margin-top:2px}
16
+ .badge{font-size:11px;font-weight:500;padding:4px 10px;border-radius:20px;letter-spacing:.02em}
17
+ .badge.approve{background:#d4edda;color:#1a7f3c}
18
+ .badge.request{background:#fde8e8;color:#c0392b}
19
+ .badge.escalate{background:#fef3cd;color:#856404}
20
+ .badge.running{background:var(--color-background-secondary);color:var(--color-text-secondary)}
21
+ .metrics{display:grid;grid-template-columns:repeat(3,1fr);gap:10px;padding:14px 18px;border-bottom:0.5px solid var(--color-border-tertiary)}
22
+ .metric-card{background:var(--color-background-secondary);border-radius:var(--border-radius-md);padding:10px 14px}
23
+ .metric-card .m-label{font-size:11px;color:var(--color-text-tertiary);margin-bottom:4px}
24
+ .metric-card .m-val{font-size:22px;font-weight:500;color:var(--color-text-primary);line-height:1}
25
+ .metric-card .m-sub{font-size:11px;color:var(--color-text-secondary);margin-top:3px}
26
+ .progress-bar{height:4px;background:var(--color-border-tertiary);border-radius:2px;overflow:hidden;margin-top:6px}
27
+ .progress-fill{height:100%;border-radius:2px;transition:width .5s ease;background:#1a7f3c}
28
+ .tabs{display:flex;gap:0;padding:0 18px;border-bottom:0.5px solid var(--color-border-tertiary)}
29
+ .tab{font-size:12px;font-weight:500;padding:9px 14px;cursor:pointer;color:var(--color-text-secondary);border-bottom:2px solid transparent;transition:color .15s}
30
+ .tab.active{color:var(--color-text-primary);border-bottom-color:var(--color-text-primary)}
31
+ .content{flex:1;overflow:auto;padding:16px 18px;display:flex;flex-direction:column;gap:14px}
32
+ .diff-box{background:#0d1117;border-radius:var(--border-radius-md);overflow:hidden;border:0.5px solid #30363d}
33
+ .diff-header{background:#161b22;padding:8px 14px;font-size:11px;color:#8b949e;font-family:var(--font-mono);border-bottom:0.5px solid #30363d;display:flex;justify-content:space-between}
34
+ .diff-body{padding:4px 0;font-family:var(--font-mono);font-size:12px;line-height:1.7}
35
+ .diff-line{padding:1px 14px;display:flex;gap:10px;color:#e6edf3}
36
+ .diff-line .ln{color:#484f58;min-width:28px;text-align:right;user-select:none}
37
+ .diff-line.add{background:rgba(46,160,67,.15);color:#aff5b4}
38
+ .diff-line.add .ln{color:rgba(46,160,67,.5)}
39
+ .diff-line.del{background:rgba(248,81,73,.15);color:#ffa198}
40
+ .diff-line.del .ln{color:rgba(248,81,73,.5)}
41
+ .diff-line.meta{color:#8b949e;background:#161b22}
42
+ .chat-thread{display:flex;flex-direction:column;gap:12px}
43
+ .chat-msg{display:flex;gap:10px;align-items:flex-start}
44
+ .chat-msg.author{flex-direction:row-reverse}
45
+ .avatar{width:30px;height:30px;border-radius:50%;display:flex;align-items:center;justify-content:center;font-size:11px;font-weight:500;flex-shrink:0}
46
+ .avatar.reviewer{background:#E6F1FB;color:#185FA5}
47
+ .avatar.author{background:#EAF3DE;color:#3B6D11}
48
+ .bubble{background:var(--color-background-secondary);border:0.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-lg);padding:10px 14px;font-size:13px;color:var(--color-text-primary);max-width:80%;line-height:1.55}
49
+ .bubble.reviewer{border-radius:4px var(--border-radius-lg) var(--border-radius-lg) var(--border-radius-lg)}
50
+ .bubble.author{background:#F0F7FF;border-radius:var(--border-radius-lg) 4px var(--border-radius-lg) var(--border-radius-lg)}
51
+ .chat-meta{font-size:10px;color:var(--color-text-tertiary);margin-top:3px}
52
+ .log-box{background:var(--color-background-secondary);border:0.5px solid var(--color-border-tertiary);border-radius:var(--border-radius-md);overflow:hidden}
53
+ .log-toggle{padding:10px 14px;font-size:12px;cursor:pointer;color:var(--color-text-secondary);display:flex;justify-content:space-between;align-items:center;user-select:none}
54
+ .log-toggle:hover{background:var(--color-background-tertiary)}
55
+ .log-content{display:none;padding:12px 14px;font-family:var(--font-mono);font-size:11px;color:var(--color-text-secondary);border-top:0.5px solid var(--color-border-tertiary);line-height:1.8}
56
+ .log-content.open{display:block}
57
+ .reward-chart{padding:4px 0 8px}
58
+ .chart-row{display:flex;align-items:center;gap:8px;margin-bottom:5px}
59
+ .chart-label{font-size:11px;color:var(--color-text-tertiary);min-width:40px;text-align:right}
60
+ .chart-bar-wrap{flex:1;height:12px;background:var(--color-background-secondary);border-radius:2px;overflow:hidden}
61
+ .chart-bar{height:100%;border-radius:2px;transition:width .6s ease}
62
+ .chart-val{font-size:11px;color:var(--color-text-secondary);min-width:28px}
63
+ .section-head{font-size:11px;font-weight:500;color:var(--color-text-tertiary);text-transform:uppercase;letter-spacing:.06em}
64
+ .pulse{animation:pulse 1.8s ease-in-out infinite}
65
+ @keyframes pulse{0%,100%{opacity:1}50%{opacity:.45}}
66
+ .thinking{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--color-text-secondary);padding:4px 0}
67
+ .dot{width:6px;height:6px;border-radius:50%;background:var(--color-text-tertiary)}
68
+ .dot:nth-child(1){animation:blink 1.2s .0s infinite}
69
+ .dot:nth-child(2){animation:blink 1.2s .2s infinite}
70
+ .dot:nth-child(3){animation:blink 1.2s .4s infinite}
71
+ @keyframes blink{0%,80%,100%{opacity:.2}40%{opacity:1}}
72
+ .sep{height:0.5px;background:var(--color-border-tertiary)}
73
+ </style>
74
+
75
+ <h2 class="sr-only">PR Review Command Center — interactive dashboard for AI code review negotiation</h2>
76
+
77
+ <div class="dash" id="dash">
78
+ <div class="sidebar">
79
+ <div>
80
+ <div class="sidebar-label">Task difficulty</div>
81
+ <select id="task-sel">
82
+ <option>Easy — fix typo in docs</option>
83
+ <option selected>Medium — fix pagination offset</option>
84
+ <option>Hard — concurrency race condition</option>
85
+ </select>
86
+ </div>
87
+ <div>
88
+ <div class="sidebar-label">API base URL</div>
89
+ <input type="text" value="https://api.anthropic.com" />
90
+ </div>
91
+ <div>
92
+ <div class="sidebar-label">Model</div>
93
+ <input type="text" value="claude-sonnet-4-20250514" />
94
+ </div>
95
+ <div class="sep"></div>
96
+ <button class="init-btn" id="init-btn" onclick="initEnv()">Initialize environment</button>
97
+ <div style="margin-top:auto">
98
+ <div class="sidebar-label" style="margin-bottom:8px">Reward history</div>
99
+ <div class="reward-chart" id="reward-chart">
100
+ <div class="chart-row"><span class="chart-label">T1</span><div class="chart-bar-wrap"><div class="chart-bar" style="width:35%;background:#e24b4a"></div></div><span class="chart-val">0.35</span></div>
101
+ <div class="chart-row"><span class="chart-label">T2</span><div class="chart-bar-wrap"><div class="chart-bar" style="width:62%;background:#ef9f27"></div></div><span class="chart-val">0.62</span></div>
102
+ <div class="chart-row"><span class="chart-label">T3</span><div class="chart-bar-wrap"><div class="chart-bar" style="width:88%;background:#1a7f3c"></div></div><span class="chart-val">0.88</span></div>
103
+ </div>
104
+ </div>
105
+ </div>
106
+
107
+ <div class="main">
108
+ <div class="topbar">
109
+ <div>
110
+ <div class="pr-title" id="pr-title">Fix pagination offset logic in user listings API</div>
111
+ <div class="pr-sub">#4821 · feat/pagination-fix · opened 2h ago by env-author</div>
112
+ </div>
113
+ <span class="badge request" id="status-badge">REQUEST_CHANGES</span>
114
+ </div>
115
+
116
+ <div class="metrics">
117
+ <div class="metric-card">
118
+ <div class="m-label">Cumulative reward</div>
119
+ <div class="m-val" id="score-val">0.62</div>
120
+ <div class="progress-bar"><div class="progress-fill" id="score-bar" style="width:62%"></div></div>
121
+ </div>
122
+ <div class="metric-card">
123
+ <div class="m-label">Turn</div>
124
+ <div class="m-val" id="turn-val">2 <span style="font-size:14px;color:var(--color-text-tertiary)">/ 3</span></div>
125
+ <div class="m-sub" id="turn-sub">Reviewer processing…</div>
126
+ </div>
127
+ <div class="metric-card">
128
+ <div class="m-label">Episode status</div>
129
+ <div class="m-val" style="font-size:14px;font-weight:500;padding-top:4px" id="ep-status">Running</div>
130
+ <div class="m-sub" id="ep-sub">AI turn active</div>
131
+ </div>
132
+ </div>
133
+
134
+ <div class="tabs">
135
+ <div class="tab active" onclick="showTab('diff',this)">Diff view</div>
136
+ <div class="tab" onclick="showTab('timeline',this)">Negotiation timeline</div>
137
+ <div class="tab" onclick="showTab('manual',this)">Manual override</div>
138
+ </div>
139
+
140
+ <div class="content">
141
+ <div id="tab-diff">
142
+ <div class="diff-box">
143
+ <div class="diff-header">
144
+ <span>src/api/users.py</span>
145
+ <span>+8 −4 lines</span>
146
+ </div>
147
+ <div class="diff-body" id="diff-body">
148
+ <div class="diff-line meta"><span class="ln">···</span><span>@@ -42,12 +42,16 @@ def get_users(page, limit):</span></div>
149
+ <div class="diff-line"><span class="ln">42</span><span> query = db.session.query(User)</span></div>
150
+ <div class="diff-line del"><span class="ln">43</span><span>- offset = page * limit</span></div>
151
+ <div class="diff-line add"><span class="ln">43</span><span>+ offset = (page - 1) * limit</span></div>
152
+ <div class="diff-line"><span class="ln">44</span><span> results = query.offset(offset).limit(limit).all()</span></div>
153
+ <div class="diff-line del"><span class="ln">45</span><span>- return results</span></div>
154
+ <div class="diff-line add"><span class="ln">45</span><span>+ if not results and page > 1:</span></div>
155
+ <div class="diff-line add"><span class="ln">46</span><span>+ raise PageOutOfRangeError(page)</span></div>
156
+ <div class="diff-line add"><span class="ln">47</span><span>+ return paginate_response(results, page, limit)</span></div>
157
+ <div class="diff-line"><span class="ln">48</span><span> </span></div>
158
+ <div class="diff-line del"><span class="ln">49</span><span>- # TODO: add error handling</span></div>
159
+ <div class="diff-line add"><span class="ln">49</span><span>+ # Handled via PageOutOfRangeError + paginate_response</span></div>
160
+ </div>
161
+ </div>
162
+ </div>
163
+
164
+ <div id="tab-timeline" style="display:none">
165
+ <div class="chat-thread">
166
+ <div class="chat-msg">
167
+ <div class="avatar reviewer">AI</div>
168
+ <div>
169
+ <div class="bubble reviewer">The offset calculation on line 43 is incorrect. For 1-indexed pagination, <code style="background:var(--color-background-tertiary);padding:1px 4px;border-radius:3px;font-size:11px">offset = page * limit</code> would skip the first page entirely. Should be <code style="background:var(--color-background-tertiary);padding:1px 4px;border-radius:3px;font-size:11px">(page - 1) * limit</code>.</div>
170
+ <div class="chat-meta">Reviewer · Turn 1</div>
171
+ </div>
172
+ </div>
173
+ <div class="chat-msg author">
174
+ <div class="avatar author">Env</div>
175
+ <div>
176
+ <div class="bubble author">Fixed the offset bug. Also added a <code style="background:rgba(255,255,255,.3);padding:1px 4px;border-radius:3px;font-size:11px">PageOutOfRangeError</code> guard and wrapped the return in <code style="background:rgba(255,255,255,.3);padding:1px 4px;border-radius:3px;font-size:11px">paginate_response()</code> for consistency. Take another look!</div>
177
+ <div class="chat-meta" style="text-align:right">Author · Turn 1 response</div>
178
+ </div>
179
+ </div>
180
+ <div class="chat-msg">
181
+ <div class="avatar reviewer">AI</div>
182
+ <div>
183
+ <div class="bubble reviewer">Good fixes. However, <code style="background:var(--color-background-tertiary);padding:1px 4px;border-radius:3px;font-size:11px">paginate_response()</code> is not defined in scope — is it imported? Missing import will cause a runtime error.</div>
184
+ <div class="chat-meta">Reviewer · Turn 2</div>
185
+ </div>
186
+ </div>
187
+ <div class="chat-msg">
188
+ <div class="avatar reviewer pulse">AI</div>
189
+ <div>
190
+ <div class="thinking"><div class="dot"></div><div class="dot"></div><div class="dot"></div><span style="margin-left:2px">Reviewer is thinking…</span></div>
191
+ </div>
192
+ </div>
193
+ </div>
194
+ </div>
195
+
196
+ <div id="tab-manual" style="display:none">
197
+ <div style="font-size:13px;color:var(--color-text-secondary);margin-bottom:12px">Act as the reviewer. Submit feedback to see how the environment responds.</div>
198
+ <textarea id="manual-input" style="width:100%;min-height:80px;font-size:13px;padding:10px;background:var(--color-background-secondary);border:0.5px solid var(--color-border-secondary);border-radius:var(--border-radius-md);color:var(--color-text-primary);resize:vertical;font-family:var(--font-sans)" placeholder="e.g. I found a potential null pointer issue in line 47…"></textarea>
199
+ <div style="display:flex;gap:8px;margin-top:8px">
200
+ <button class="init-btn" onclick="sendManual('REQUEST_CHANGES')">Request changes</button>
201
+ <button class="init-btn active" onclick="sendManual('APPROVE')">Approve</button>
202
+ <button class="init-btn" style="border-color:#856404;color:#856404" onclick="sendManual('ESCALATE')">Escalate</button>
203
+ </div>
204
+ <div id="manual-response" style="margin-top:14px;display:none">
205
+ <div class="section-head" style="margin-bottom:8px">Environment response</div>
206
+ <div class="bubble reviewer" id="manual-resp-text" style="max-width:100%"></div>
207
+ </div>
208
+ </div>
209
+
210
+ <div class="log-box">
211
+ <div class="log-toggle" onclick="toggleLog(this)">
212
+ <span>Behind the scenes — raw logs</span>
213
+ <span id="log-arrow">▶</span>
214
+ </div>
215
+ <div class="log-content" id="log-content">
216
+ [START] episode_id=ep_4821 task=medium seed=42
217
+ [ENV] observation: diff loaded, 8 hunks
218
+ [STEP] turn=1 action=REQUEST_CHANGES reward=0.35
219
+ [AI] tokens_used=312 latency_ms=840
220
+ [STEP] turn=2 action=REQUEST_CHANGES reward=0.62
221
+ [AI] tokens_used=289 latency_ms=760
222
+ [STATE] {"page":2,"max_turns":3,"cumulative_reward":0.62,"done":false}
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </div>
227
+ </div>
228
+
229
+ <script>
230
+ function showTab(name, el) {
231
+ ['diff','timeline','manual'].forEach(t => {
232
+ document.getElementById('tab-'+t).style.display = t===name?'':'none';
233
+ });
234
+ document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
235
+ el.classList.add('active');
236
+ }
237
+ function toggleLog(el) {
238
+ const c = document.getElementById('log-content');
239
+ const a = document.getElementById('log-arrow');
240
+ const open = c.classList.toggle('open');
241
+ a.textContent = open ? '▼' : '▶';
242
+ }
243
+ function initEnv() {
244
+ const btn = document.getElementById('init-btn');
245
+ const tasks = ['Easy — fix typo in docs','Medium — fix pagination offset','Hard — concurrency race condition'];
246
+ const sel = document.getElementById('task-sel').value;
247
+ const titles = {
248
+ 'Easy': 'Fix spelling in README introduction',
249
+ 'Medium': 'Fix pagination offset logic in user listings API',
250
+ 'Hard': 'Resolve race condition in concurrent write handler'
251
+ };
252
+ const key = sel.startsWith('Easy')?'Easy':sel.startsWith('Medium')?'Medium':'Hard';
253
+ document.getElementById('pr-title').textContent = titles[key];
254
+ document.getElementById('score-val').textContent = '0.00';
255
+ document.getElementById('score-bar').style.width = '0%';
256
+ document.getElementById('turn-val').innerHTML = '1 <span style="font-size:14px;color:var(--color-text-tertiary)">/ 3</span>';
257
+ document.getElementById('turn-sub').textContent = 'Starting…';
258
+ document.getElementById('ep-status').textContent = 'Initializing';
259
+ document.getElementById('ep-sub').textContent = 'Episode reset';
260
+ document.getElementById('status-badge').className = 'badge running';
261
+ document.getElementById('status-badge').textContent = 'RUNNING';
262
+ btn.textContent = 'Initializing…';
263
+ btn.classList.add('active');
264
+ setTimeout(() => {
265
+ btn.textContent = 'Environment ready';
266
+ document.getElementById('ep-status').textContent = 'Running';
267
+ document.getElementById('ep-sub').textContent = 'AI turn active';
268
+ document.getElementById('turn-sub').textContent = 'Reviewer processing…';
269
+ }, 1200);
270
+ }
271
+ const envResponses = {
272
+ REQUEST_CHANGES: "Acknowledged. I've addressed the points raised — updated the import for paginate_response and added a unit test. Please take another look.",
273
+ APPROVE: "Thanks for the approval! Merging into main. All checks passed.",
274
+ ESCALATE: "Understood, escalating to the senior reviewer. I'll add more context to the PR description."
275
+ };
276
+ function sendManual(decision) {
277
+ const input = document.getElementById('manual-input').value.trim();
278
+ const badges = {REQUEST_CHANGES:'request',APPROVE:'approve',ESCALATE:'escalate'};
279
+ document.getElementById('status-badge').className = 'badge ' + badges[decision];
280
+ document.getElementById('status-badge').textContent = decision;
281
+ const resp = document.getElementById('manual-response');
282
+ const txt = document.getElementById('manual-resp-text');
283
+ resp.style.display = 'block';
284
+ txt.textContent = '…';
285
+ setTimeout(() => { txt.textContent = envResponses[decision]; }, 700);
286
+ }
287
+ </script>
pr_review_dashboard/lib/api.js ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const BASE = "/api/env";
2
+
3
+ export async function resetEnv(taskName) {
4
+ const res = await fetch(`${BASE}/reset`, {
5
+ method: "POST",
6
+ headers: { "Content-Type": "application/json" },
7
+ body: JSON.stringify({ task_name: taskName }),
8
+ });
9
+ if (!res.ok) throw new Error(`Reset failed: ${res.status}`);
10
+ return res.json();
11
+ }
12
+
13
+ export async function stepEnv(action) {
14
+ const res = await fetch(`${BASE}/step`, {
15
+ method: "POST",
16
+ headers: { "Content-Type": "application/json" },
17
+ body: JSON.stringify({ action }),
18
+ });
19
+ if (!res.ok) throw new Error(`Step failed: ${res.status}`);
20
+ return res.json();
21
+ }
22
+
23
+ export async function configCustom({ diff, pr_title, pr_description }) {
24
+ const res = await fetch(`${BASE}/config/custom`, {
25
+ method: "POST",
26
+ headers: { "Content-Type": "application/json" },
27
+ body: JSON.stringify({ diff, pr_title, pr_description }),
28
+ });
29
+ if (!res.ok) throw new Error(`Config failed: ${res.status}`);
30
+ return res.json();
31
+ }
32
+
33
+ export async function callAgent({ observation, modelId, apiUrl, apiKey }) {
34
+ const res = await fetch("/api/agent", {
35
+ method: "POST",
36
+ headers: { "Content-Type": "application/json" },
37
+ body: JSON.stringify({ observation, modelId, apiUrl, apiKey }),
38
+ });
39
+ if (!res.ok) throw new Error(`Agent failed: ${res.status}`);
40
+ return res.json();
41
+ }