3v324v23 commited on
Commit
aa10350
·
1 Parent(s): f7fdd7c

V4: AI-Driven Auto-Diff — Zero-effort Red/Green suggestions and 1-click 'Apply Fix' button

Browse files
pr_review_dashboard/app/Dashboard.js CHANGED
@@ -28,6 +28,7 @@ export default function Dashboard({ presets, defaultHfToken }) {
28
  const [initialized, setInitialized] = useState(false);
29
  const [initStatus, setInitStatus] = useState("idle");
30
  const [observation, setObservation] = useState({});
 
31
  const [score, setScore] = useState(0);
32
  const [turn, setTurn] = useState(0);
33
  const [maxTurns, setMaxTurns] = useState(3);
@@ -42,11 +43,32 @@ export default function Dashboard({ presets, defaultHfToken }) {
42
  setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
43
  }, []);
44
 
45
- // ── Initialize (System Check) ──
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  const handleInit = useCallback(async () => {
47
  setError(null);
48
  setInitStatus("loading");
49
- addLog(`System Connection Test: ${activeModelId}`);
50
  try {
51
  await resetEnv(taskName);
52
  setInitialized(true);
@@ -55,179 +77,146 @@ export default function Dashboard({ presets, defaultHfToken }) {
55
  } catch (e) {
56
  setError(`Connection Failed: ${e.message}`);
57
  setInitStatus("idle");
58
- addLog(`CONNECTION ERROR: ${e.message}`);
59
  }
60
- }, [taskName, activeModelId, addLog]);
61
 
62
  // ── Manual Decision ──
63
  const handleManual = useCallback(async ({ decision: dec, comment }) => {
64
  if (done) return;
65
  setError(null);
66
- addLog(`User Decision: ${dec}`);
67
  try {
68
- const result = await stepEnv({
69
- decision: dec,
70
- comment: comment || "Manual verdict.",
71
- issue_category: "none" // Default for manual verdicts
72
- });
73
  setObservation(result.observation);
74
  setScore(prev => prev + result.reward);
75
  setRewards(prev => [...prev, result.reward]);
76
  setDone(result.done);
77
  setDecision(dec.toUpperCase());
78
  setTurn(result.observation.turn);
79
- addLog(`VERDICT APPLIED: reward=${result.reward.toFixed(2)} done=${result.done}`);
80
- } catch (e) {
81
- setError(e.message);
82
- addLog(`VERDICT ERROR: ${e.message}`);
83
- }
84
- }, [done, addLog]);
85
 
86
- // ── Execute AI Round ──
87
  const handleExecute = useCallback(async () => {
88
  if (done || isThinking) return;
89
  setError(null);
90
  setIsThinking(true);
91
- addLog(`Requesting AI Analysis (${activeModelId})...`);
92
  try {
93
- const action = await callAgent({
94
- observation, modelId: activeModelId, apiUrl: activeApiUrl, apiKey: activeApiKey,
95
- });
96
  if (action.decision === "error") throw new Error(action.comment);
97
 
98
- addLog(`AI Feedback Received: ${action.decision}`);
99
  const result = await stepEnv(action);
100
- setObservation(result.observation);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
101
  setScore(prev => prev + result.reward);
102
  setRewards(prev => [...prev, result.reward]);
103
  setDone(result.done);
104
  setDecision(action.decision.toUpperCase());
105
  setTurn(result.observation.turn);
106
- addLog(`STEP SUCCESS: reward=${result.reward.toFixed(2)}`);
107
- } catch (e) {
108
- setError(e.message);
109
- addLog(`AI ERROR: ${e.message}`);
110
- } finally {
111
- setIsThinking(false);
112
- }
113
- }, [done, isThinking, observation, activeModelId, activeApiUrl, activeApiKey, addLog]);
 
 
 
 
 
 
 
 
 
114
 
115
- // ── Handle Code Submission (The One-Push Trigger) ──
116
  const handleCodeSubmit = useCallback(async (code) => {
117
  setError(null);
118
  setIsThinking(true);
119
- addLog("PHASE 2: Parsing submitted code...");
120
  try {
121
- // 1. Configure the scenario
122
  await configCustom({ diff: code, pr_title: customTitle, pr_description: customDesc });
123
-
124
- // 2. Initialize the specific environment
125
  const obs = await resetEnv("custom-review");
126
 
127
- // 3. Reset state for new session
128
- setObservation(obs);
129
- setInitialized(true);
130
- setInitStatus("ready");
131
- setScore(0);
132
- setTurn(1);
133
- setMaxTurns(obs.max_turns || 3);
134
- setDone(false);
135
- setDecision("IDLE");
136
- setRewards([]);
137
- setTaskName("custom-review");
138
-
139
- addLog("CODE LOADED. Auto-triggering AI Review round...");
140
-
141
- // 4. AUTO-START THE REVIEW (The real "One-Push")
142
- const action = await callAgent({
143
- observation: obs, modelId: activeModelId, apiUrl: activeApiUrl, apiKey: activeApiKey,
144
- });
145
- if (action.decision === "error") throw new Error(action.comment);
146
-
147
  const result = await stepEnv(action);
148
- setObservation(result.observation);
149
- setScore(result.reward);
150
- setRewards([result.reward]);
151
- setDone(result.done);
152
- setTurn(result.observation.turn); // Fix: Sync directly with backend
153
- setDecision(action.decision.toUpperCase());
154
- addLog(`AI REVIEW COMPLETE. Check results below.`);
155
- } catch (e) {
156
- setError(e.message);
157
- addLog(`SESSION ERROR: ${e.message}`);
158
- } finally {
159
- setIsThinking(false);
160
- }
161
- }, [customTitle, customDesc, activeModelId, activeApiUrl, activeApiKey, addLog]);
162
 
163
- const epStatus = !initialized ? "Offline" : done ? "Reviewed" : "Active";
164
- const epSub = !initialized ? "Waiting for system check" : done ? "History saved" : isThinking ? "Reviewer processing..." : "Awaiting decision";
165
- const prSub = initialized ? `${taskName} · Session Progress` : "Connect to begin";
 
 
 
 
 
166
 
167
- // Decision state for TopBar
168
- const currentDecision = decision;
 
 
 
 
 
 
 
169
 
170
  return (
171
  <div className="dash">
172
- <Sidebar
173
- taskName={taskName} setTaskName={setTaskName}
174
- presets={presets}
175
  selectedPreset={selectedPreset} setSelectedPreset={setSelectedPreset}
176
  customApiUrl={customApiUrl} setCustomApiUrl={setCustomApiUrl}
177
  customModelId={customModelId} setCustomModelId={setCustomModelId}
178
  customApiKey={customApiKey} setCustomApiKey={setCustomApiKey}
179
- isInternal={isInternal}
180
- onInit={handleInit}
181
- initStatus={initStatus}
182
- rewards={rewards}
183
- customTitle={customTitle} setCustomTitle={setCustomTitle}
184
  customDesc={customDesc} setCustomDesc={setCustomDesc}
185
  />
186
  <div className="main">
187
- <TopBar
188
- title={observation.pr_title || "PR Review Command Center"}
189
- subtitle={prSub}
190
- decision={currentDecision}
191
- />
192
- <MetricCards score={score} turn={turn} maxTurns={maxTurns} status={epStatus} statusSub={epSub} />
193
-
194
  <div className="content unified-workspace">
195
  {error && <div className="status-msg error">{error}</div>}
196
-
197
  <div className="workspace-layout">
198
- {/* Stage 1: The Input Panel (Visible when no code loaded) */}
199
  {(!observation.diff || !initialized) ? (
200
  <div className="full-width-input">
201
- <DiffView
202
- diff={null}
203
- onCodeSubmit={handleCodeSubmit}
204
- isProcessing={isThinking}
205
- />
206
  </div>
207
  ) : (
208
- /* Stage 2 & 3: Parallel Review View */
209
  <div className="split-view">
210
  <div className="split-left">
211
  <div className="pane-header">CODE CHANGES</div>
212
  <DiffView
213
  diff={observation.diff}
214
  isAccepted={done && decision === 'APPROVE'}
 
 
215
  />
216
  </div>
217
  <div className="split-right">
218
  <div className="pane-header">NEGOTIATION TIMELINE</div>
219
- <Timeline
220
- history={observation.review_history || []}
221
- isThinking={isThinking}
222
- onExecute={handleExecute}
223
- onManual={handleManual}
224
- done={done}
225
- />
226
  </div>
227
  </div>
228
  )}
229
  </div>
230
-
231
  <LogBox logs={logs} />
232
  </div>
233
  </div>
 
28
  const [initialized, setInitialized] = useState(false);
29
  const [initStatus, setInitStatus] = useState("idle");
30
  const [observation, setObservation] = useState({});
31
+ const [originalCode, setOriginalCode] = useState(""); // Track the user's base code
32
  const [score, setScore] = useState(0);
33
  const [turn, setTurn] = useState(0);
34
  const [maxTurns, setMaxTurns] = useState(3);
 
43
  setLogs(prev => [...prev, `[${new Date().toLocaleTimeString()}] ${msg}`]);
44
  }, []);
45
 
46
+ // ── Helper: Fetch Diff from Backend ──
47
+ const getVisualDiff = async (oldCode, newCode) => {
48
+ try {
49
+ const resp = await fetch("/api/diff", {
50
+ method: "POST",
51
+ headers: { "Content-Type": "application/json" },
52
+ body: JSON.stringify({ old_code: oldCode, new_code: newCode })
53
+ });
54
+ const data = await resp.json();
55
+ return data.diff;
56
+ } catch (e) {
57
+ console.error("Diff generation failed", e);
58
+ return null;
59
+ }
60
+ };
61
+
62
+ // ── Helper: Extract Code Block from AI response ──
63
+ const extractProposedFix = (text) => {
64
+ const match = text.match(/```python\n([\s\S]*?)\n```/);
65
+ return match ? match[1] : null;
66
+ };
67
+
68
+ // ── Initialize ──
69
  const handleInit = useCallback(async () => {
70
  setError(null);
71
  setInitStatus("loading");
 
72
  try {
73
  await resetEnv(taskName);
74
  setInitialized(true);
 
77
  } catch (e) {
78
  setError(`Connection Failed: ${e.message}`);
79
  setInitStatus("idle");
 
80
  }
81
+ }, [taskName, addLog]);
82
 
83
  // ── Manual Decision ──
84
  const handleManual = useCallback(async ({ decision: dec, comment }) => {
85
  if (done) return;
86
  setError(null);
 
87
  try {
88
+ const result = await stepEnv({ decision: dec, comment: comment || "Manual verdict.", issue_category: "none" });
 
 
 
 
89
  setObservation(result.observation);
90
  setScore(prev => prev + result.reward);
91
  setRewards(prev => [...prev, result.reward]);
92
  setDone(result.done);
93
  setDecision(dec.toUpperCase());
94
  setTurn(result.observation.turn);
95
+ } catch (e) { setError(e.message); }
96
+ }, [done]);
 
 
 
 
97
 
98
+ // ── Execute AI Round (with Auto-Diff logic) ──
99
  const handleExecute = useCallback(async () => {
100
  if (done || isThinking) return;
101
  setError(null);
102
  setIsThinking(true);
103
+ addLog(`AI reviewing current state...`);
104
  try {
105
+ const action = await callAgent({ observation, modelId: activeModelId, apiUrl: activeApiUrl, apiKey: activeApiKey });
 
 
106
  if (action.decision === "error") throw new Error(action.comment);
107
 
 
108
  const result = await stepEnv(action);
109
+ let updatedObs = result.observation;
110
+
111
+ // --- AUTO-DIFF ENHANCEMENT ---
112
+ // If AI requested changes and provided a fix, we generate a visual diff automatically!
113
+ if (action.decision === "request_changes") {
114
+ const fix = extractProposedFix(action.comment);
115
+ if (fix && originalCode) {
116
+ const aiDiff = await getVisualDiff(originalCode, fix);
117
+ if (aiDiff) {
118
+ updatedObs = { ...updatedObs, diff: aiDiff, isAiProposal: true, proposedCode: fix };
119
+ addLog("AI generated a visual proposal. View the 'Code Changes' tab.");
120
+ }
121
+ }
122
+ }
123
+
124
+ setObservation(updatedObs);
125
  setScore(prev => prev + result.reward);
126
  setRewards(prev => [...prev, result.reward]);
127
  setDone(result.done);
128
  setDecision(action.decision.toUpperCase());
129
  setTurn(result.observation.turn);
130
+ } catch (e) { setError(e.message); }
131
+ finally { setIsThinking(false); }
132
+ }, [done, isThinking, observation, originalCode, activeModelId, activeApiUrl, activeApiKey, addLog]);
133
+
134
+ // ── Apply AI Suggestion (Zero-effort update) ──
135
+ const handleApplyFix = useCallback(async () => {
136
+ if (!observation.proposedCode) return;
137
+ addLog("Applying AI fix to working code...");
138
+ try {
139
+ // Re-configure the scenario with the new code
140
+ await configCustom({ diff: observation.proposedCode, pr_title: customTitle, pr_description: customDesc });
141
+ // Reset the current view to show the 'Clean' version of the fix
142
+ setOriginalCode(observation.proposedCode);
143
+ setObservation(prev => ({ ...prev, diff: observation.proposedCode, isAiProposal: false }));
144
+ addLog("Fix applied. You can now start another review round if needed.");
145
+ } catch (e) { setError(e.message); }
146
+ }, [observation, customTitle, customDesc, addLog]);
147
 
148
+ // ── One-Push Implementation ──
149
  const handleCodeSubmit = useCallback(async (code) => {
150
  setError(null);
151
  setIsThinking(true);
152
+ setOriginalCode(code); // Store the base code
153
  try {
 
154
  await configCustom({ diff: code, pr_title: customTitle, pr_description: customDesc });
 
 
155
  const obs = await resetEnv("custom-review");
156
 
157
+ const action = await callAgent({ observation: obs, modelId: activeModelId, apiUrl: activeApiUrl, apiKey: activeApiKey });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
158
  const result = await stepEnv(action);
159
+ let finalObs = result.observation;
 
 
 
 
 
 
 
 
 
 
 
 
 
160
 
161
+ // Generate Auto-Diff if first round identifies issues
162
+ if (action.decision === "request_changes") {
163
+ const fix = extractProposedFix(action.comment);
164
+ if (fix) {
165
+ const aiDiff = await getVisualDiff(code, fix);
166
+ if (aiDiff) finalObs = { ...finalObs, diff: aiDiff, isAiProposal: true, proposedCode: fix };
167
+ }
168
+ }
169
 
170
+ setObservation(finalObs);
171
+ setInitialized(true); setInitStatus("ready");
172
+ setScore(result.reward); setTurn(result.observation.turn);
173
+ setMaxTurns(obs.max_turns || 3); setDone(false);
174
+ setDecision(action.decision.toUpperCase()); setRewards([result.reward]);
175
+ setTaskName("custom-review");
176
+ } catch (e) { setError(e.message); }
177
+ finally { setIsThinking(false); }
178
+ }, [customTitle, customDesc, activeModelId, activeApiUrl, activeApiKey, addLog]);
179
 
180
  return (
181
  <div className="dash">
182
+ <Sidebar
183
+ taskName={taskName} setTaskName={setTaskName} presets={presets}
 
184
  selectedPreset={selectedPreset} setSelectedPreset={setSelectedPreset}
185
  customApiUrl={customApiUrl} setCustomApiUrl={setCustomApiUrl}
186
  customModelId={customModelId} setCustomModelId={setCustomModelId}
187
  customApiKey={customApiKey} setCustomApiKey={setCustomApiKey}
188
+ isInternal={isInternal} onInit={handleInit} initStatus={initStatus}
189
+ rewards={rewards} customTitle={customTitle} setCustomTitle={setCustomTitle}
 
 
 
190
  customDesc={customDesc} setCustomDesc={setCustomDesc}
191
  />
192
  <div className="main">
193
+ <TopBar title={observation.pr_title || "PR Review Command Center"} subtitle={initialized ? `${taskName} · Session Progress` : "Connect to begin"} decision={decision} />
194
+ <MetricCards score={score} turn={turn} maxTurns={maxTurns} status={!initialized ? "Offline" : done ? "Reviewed" : "Active"} statusSub={done ? "History saved" : isThinking ? "Reviewer processing..." : "Awaiting decision"} />
 
 
 
 
 
195
  <div className="content unified-workspace">
196
  {error && <div className="status-msg error">{error}</div>}
 
197
  <div className="workspace-layout">
 
198
  {(!observation.diff || !initialized) ? (
199
  <div className="full-width-input">
200
+ <DiffView diff={null} onCodeSubmit={handleCodeSubmit} isProcessing={isThinking} />
 
 
 
 
201
  </div>
202
  ) : (
 
203
  <div className="split-view">
204
  <div className="split-left">
205
  <div className="pane-header">CODE CHANGES</div>
206
  <DiffView
207
  diff={observation.diff}
208
  isAccepted={done && decision === 'APPROVE'}
209
+ isAiProposal={observation.isAiProposal}
210
+ onApplyFix={handleApplyFix}
211
  />
212
  </div>
213
  <div className="split-right">
214
  <div className="pane-header">NEGOTIATION TIMELINE</div>
215
+ <Timeline history={observation.review_history || []} isThinking={isThinking} onExecute={handleExecute} onManual={handleManual} done={done} />
 
 
 
 
 
 
216
  </div>
217
  </div>
218
  )}
219
  </div>
 
220
  <LogBox logs={logs} />
221
  </div>
222
  </div>
pr_review_dashboard/app/api/agent/route.js CHANGED
@@ -10,14 +10,19 @@ export async function POST(request) {
10
 
11
  const client = new OpenAI({ baseURL: apiUrl, apiKey: apiKey });
12
 
13
- const systemPrompt =
14
- 'You are a senior software engineer performing a pull request code review.\n' +
15
- 'Respond with ONLY this JSON (no markdown, no extra text):\n' +
16
- '{\n' +
17
- ' "decision": "approve|request_changes|escalate",\n' +
18
- ' "issue_category": "logic|security|performance|correctness|none",\n' +
19
- ' "comment": "your detailed review identifying root cause"\n' +
20
- '}';
 
 
 
 
 
21
 
22
  const historyLines = (observation.review_history || [])
23
  .map(h => `${h.role.toUpperCase()}: ${h.content}`)
 
10
 
11
  const client = new OpenAI({ baseURL: apiUrl, apiKey: apiKey });
12
 
13
+ const systemPrompt = `You are a Senior Software Engineer conducting a thorough Pull Request review.
14
+ Your goal is to evaluate the provided diff and issue a verdict: [approve], [request_changes], or [escalate].
15
+
16
+ CRITICAL:
17
+ 1. Focus on root causes (e.g., SQL injection, Race conditions, Logic errors), not just style.
18
+ 2. If you issue [request_changes], you MUST include a "Proposed Fix" code block (using \`\`\`python) that shows the full CORRECTED version of the logic you are criticizing.
19
+
20
+ RESPONSE FORMAT (VALID JSON):
21
+ {
22
+ "decision": "approve" | "request_changes" | "escalate",
23
+ "comment": "Detailed technical feedback here. Mention specific line numbers and root causes.",
24
+ "issue_category": "security" | "logic" | "performance" | "style" | "none"
25
+ }`;
26
 
27
  const historyLines = (observation.review_history || [])
28
  .map(h => `${h.role.toUpperCase()}: ${h.content}`)
pr_review_dashboard/app/api/diff/route.js ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ export async function POST(req) {
2
+ try {
3
+ const body = await req.json();
4
+ const backendUrl = process.env.API_BASE_URL || "http://localhost:8000";
5
+
6
+ const response = await fetch(`${backendUrl}/diff`, {
7
+ method: "POST",
8
+ headers: { "Content-Type": "application/json" },
9
+ body: JSON.stringify(body),
10
+ });
11
+
12
+ const data = await response.json();
13
+ return Response.json(data);
14
+ } catch (error) {
15
+ return Response.json({ error: error.message }, { status: 500 });
16
+ }
17
+ }
pr_review_dashboard/components/DiffView.js CHANGED
@@ -48,7 +48,7 @@ function highlightCode(text) {
48
  return finalHtml;
49
  }
50
 
51
- export default function DiffView({ diff, onCodeSubmit, isProcessing, isAccepted }) {
52
  const [inputText, setInputText] = useState("");
53
  const [isDragOver, setIsDragOver] = useState(false);
54
 
@@ -163,13 +163,24 @@ export default function DiffView({ diff, onCodeSubmit, isProcessing, isAccepted
163
 
164
  return (
165
  <div className="diff-box">
166
- <div className="diff-header" style={{ borderLeft: '2px solid transparent' }}>
167
- <span style={{ color: isAccepted ? "#3fb950" : "inherit" }}>
168
- {isAccepted ? `✓ ${filename} (Concluded)` : filename}
169
- </span>
170
- <span style={{ fontSize: '10px', color: '#8b949e' }}>
171
- {isAccepted ? "HISTORY PRESERVED" : `+${adds} −${dels} lines`}
172
  </span>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
173
  </div>
174
  <div className="diff-body" style={{ minHeight: '400px' }}>
175
  {parsedLines.map((line, i) => (
 
48
  return finalHtml;
49
  }
50
 
51
+ export default function DiffView({ diff, onCodeSubmit, isProcessing, isAccepted, isAiProposal, onApplyFix }) {
52
  const [inputText, setInputText] = useState("");
53
  const [isDragOver, setIsDragOver] = useState(false);
54
 
 
163
 
164
  return (
165
  <div className="diff-box">
166
+ <div className="diff-header" style={{ borderLeft: isAiProposal ? '4px solid #ff7b72' : '2px solid transparent' }}>
167
+ <span style={{ color: isAccepted ? "#3fb950" : isAiProposal ? "#ff7b72" : "inherit" }}>
168
+ {isAccepted ? `✓ ${filename} (Concluded)` : isAiProposal ? `⚠️ AI PROPOSAL: ${filename}` : filename}
 
 
 
169
  </span>
170
+ <div style={{ display: 'flex', alignItems: 'center', gap: '10px' }}>
171
+ <span style={{ fontSize: '10px', color: '#8b949e' }}>
172
+ {isAccepted ? "HISTORY PRESERVED" : `+${adds} −${dels} lines`}
173
+ </span>
174
+ {isAiProposal && onApplyFix && (
175
+ <button
176
+ className="hunk-btn accept"
177
+ onClick={onApplyFix}
178
+ style={{ fontSize: '9px', padding: '2px 8px' }}
179
+ >
180
+ Apply AI Suggestion →
181
+ </button>
182
+ )}
183
+ </div>
184
  </div>
185
  <div className="diff-body" style={{ minHeight: '400px' }}>
186
  {parsedLines.map((line, i) => (
server/app.py CHANGED
@@ -61,6 +61,22 @@ def state():
61
  raise HTTPException(status_code=400, detail="No active episode. Call /reset first.")
62
  return env.state()
63
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
64
  def main():
65
  import uvicorn
66
  import os
 
61
  raise HTTPException(status_code=400, detail="No active episode. Call /reset first.")
62
  return env.state()
63
 
64
+ @app.post("/diff")
65
+ async def generate_diff(payload: dict):
66
+ old_code = payload.get("old_code", "")
67
+ new_code = payload.get("new_code", "")
68
+ filename = payload.get("filename", "file.py")
69
+
70
+ import difflib
71
+ old_lines = old_code.splitlines(keepends=True)
72
+ new_lines = new_code.splitlines(keepends=True)
73
+ diff = difflib.unified_diff(
74
+ old_lines, new_lines,
75
+ fromfile=f"a/{filename}",
76
+ tofile=f"b/{filename}"
77
+ )
78
+ return {"diff": "".join(diff)}
79
+
80
  def main():
81
  import uvicorn
82
  import os