E5K7 commited on
Commit
342230a
·
1 Parent(s): 661a588

feat: Next.js frontend with live IDE, URL routing, retry logic, and step tracker

Browse files

- Add Next.js frontend with 3-pane IDE layout (explorer, editor, discovery)
- Implement URL-based routing: /owner/repo auto-loads repository
- Add live file highlighting as agent analyzes codebase
- Add VS Code-style bottom terminal drawer with step progress tracker
- Stream fix code directly into editor during step 4 (fix generation)
- Show fixedFileView in editor after analysis completes
- Add Result Actions panel with PR button and refinement chat
- Add retry with exponential backoff for GLM 429 rate limit errors
- Add inter-step delays to prevent burst rate limiting
- Fix hydration mismatch from browser extensions (suppressHydrationWarning)
- Add /api/file_content endpoint for browsing repo source files
- Make layout fit 100vh with compact top bar

backend/agent.py CHANGED
@@ -138,12 +138,13 @@ class FixFlowAgent:
138
  t1 = time.time()
139
 
140
  result.bug_summary = self._step1_issue_understanding(
141
- result.issue_data, stream_callback
142
  )
143
  result.step_timings["1_issue"] = time.time() - t1
144
  self._emit("1_issue", "complete",
145
  f"Bug analysis complete in {result.step_timings['1_issue']:.1f}s")
146
 
 
147
  # ── Step 2: Codebase Mapping ──────────────────────────────────
148
  self._emit("2_mapping", "running", "Scanning codebase to identify suspect files...")
149
  t2 = time.time()
@@ -153,7 +154,7 @@ class FixFlowAgent:
153
  result.bug_summary,
154
  result.file_tree,
155
  result.issue_data,
156
- stream_callback,
157
  repo_url=repo_url,
158
  )
159
  result.step_timings["2_mapping"] = time.time() - t2
@@ -161,6 +162,7 @@ class FixFlowAgent:
161
  f"Identified {len(result.suspect_file_paths)} suspect files in "
162
  f"{result.step_timings['2_mapping']:.1f}s")
163
 
 
164
  # ── Step 3: Deep Code Analysis ────────────────────────────────
165
  self._emit("3_analysis", "running",
166
  f"Reading {len(result.suspect_file_paths)} files + performing root cause analysis...")
@@ -172,12 +174,13 @@ class FixFlowAgent:
172
  result.root_cause_analysis = self._step3_deep_analysis(
173
  result.bug_summary,
174
  result.original_file_contents,
175
- stream_callback,
176
  )
177
  result.step_timings["3_analysis"] = time.time() - t3
178
  self._emit("3_analysis", "complete",
179
  f"Root cause identified in {result.step_timings['3_analysis']:.1f}s")
180
 
 
181
  # ── Step 4: Fix Generation ────────────────────────────────────
182
  self._emit("4_fix", "running", "Generating corrected file contents...")
183
  t4 = time.time()
@@ -196,6 +199,7 @@ class FixFlowAgent:
196
  f"Generated fixes for {len(result.fixed_files)} file(s) in "
197
  f"{result.step_timings['4_fix']:.1f}s")
198
 
 
199
  # ── Step 5: Diff & Explanation ────────────────────────────────
200
  self._emit("5_diff", "running", "Generating diff and PR explanation...")
201
  t5 = time.time()
@@ -225,7 +229,7 @@ class FixFlowAgent:
225
  f"# Root Cause\n{result.root_cause_analysis}\n\n"
226
  f"# Fix Explanation\n{result.fix_explanation}"
227
  )
228
- result.confidence_eval = self._run_confidence_eval(combined)
229
  result.step_timings["6_confidence"] = time.time() - t6
230
  self._emit("6_confidence", "complete",
231
  f"Confidence eval done in {result.step_timings['6_confidence']:.1f}s")
@@ -239,6 +243,75 @@ class FixFlowAgent:
239
 
240
  return result
241
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
242
  # ── Pipeline Steps ────────────────────────────────────────────────────────
243
 
244
  def _step1_issue_understanding(
@@ -381,7 +454,7 @@ class FixFlowAgent:
381
  ]
382
  return self._llm_call(messages, stream_cb, temperature=0.3)
383
 
384
- def _run_confidence_eval(self, analysis: str) -> str:
385
  self._current_step = "6_confidence"
386
  prompt = CONFIDENCE_EVAL_PROMPT.format(analysis=analysis[:4000])
387
  messages = [
 
138
  t1 = time.time()
139
 
140
  result.bug_summary = self._step1_issue_understanding(
141
+ result.issue_data, None # reasoning step — don't stream to UI
142
  )
143
  result.step_timings["1_issue"] = time.time() - t1
144
  self._emit("1_issue", "complete",
145
  f"Bug analysis complete in {result.step_timings['1_issue']:.1f}s")
146
 
147
+ time.sleep(2) # avoid burst rate-limiting
148
  # ── Step 2: Codebase Mapping ──────────────────────────────────
149
  self._emit("2_mapping", "running", "Scanning codebase to identify suspect files...")
150
  t2 = time.time()
 
154
  result.bug_summary,
155
  result.file_tree,
156
  result.issue_data,
157
+ None, # reasoning step — don't stream to UI
158
  repo_url=repo_url,
159
  )
160
  result.step_timings["2_mapping"] = time.time() - t2
 
162
  f"Identified {len(result.suspect_file_paths)} suspect files in "
163
  f"{result.step_timings['2_mapping']:.1f}s")
164
 
165
+ time.sleep(2) # avoid burst rate-limiting
166
  # ── Step 3: Deep Code Analysis ────────────────────────────────
167
  self._emit("3_analysis", "running",
168
  f"Reading {len(result.suspect_file_paths)} files + performing root cause analysis...")
 
174
  result.root_cause_analysis = self._step3_deep_analysis(
175
  result.bug_summary,
176
  result.original_file_contents,
177
+ None, # reasoning step — don't stream to UI
178
  )
179
  result.step_timings["3_analysis"] = time.time() - t3
180
  self._emit("3_analysis", "complete",
181
  f"Root cause identified in {result.step_timings['3_analysis']:.1f}s")
182
 
183
+ time.sleep(2) # avoid burst rate-limiting
184
  # ── Step 4: Fix Generation ────────────────────────────────────
185
  self._emit("4_fix", "running", "Generating corrected file contents...")
186
  t4 = time.time()
 
199
  f"Generated fixes for {len(result.fixed_files)} file(s) in "
200
  f"{result.step_timings['4_fix']:.1f}s")
201
 
202
+ time.sleep(2) # avoid burst rate-limiting
203
  # ── Step 5: Diff & Explanation ────────────────────────────────
204
  self._emit("5_diff", "running", "Generating diff and PR explanation...")
205
  t5 = time.time()
 
229
  f"# Root Cause\n{result.root_cause_analysis}\n\n"
230
  f"# Fix Explanation\n{result.fix_explanation}"
231
  )
232
+ result.confidence_eval = self._run_confidence_eval(combined) # don't stream
233
  result.step_timings["6_confidence"] = time.time() - t6
234
  self._emit("6_confidence", "complete",
235
  f"Confidence eval done in {result.step_timings['6_confidence']:.1f}s")
 
243
 
244
  return result
245
 
246
+ def refine_fix(
247
+ self,
248
+ feedback: str,
249
+ result: AgentResult,
250
+ on_status: StatusCallback = None,
251
+ stream_callback: Optional[Callable[[str], None]] = None,
252
+ ) -> AgentResult:
253
+ """
254
+ Re-runs Step 4 and Step 5 by appending user feedback to the existing context.
255
+ Modifies and returns the same AgentResult object.
256
+ """
257
+ self._status = on_status or (lambda *a: None)
258
+
259
+ try:
260
+ # ── Refine Fix Generation ─────────────────────────────────────
261
+ self._emit("4_refine", "running", "Refining the code based on your feedback...")
262
+ t4 = time.time()
263
+
264
+ # We append the feedback to the root cause to guide the fix generation
265
+ refined_root_cause = (
266
+ result.root_cause_analysis +
267
+ f"\n\n[USER FEEDBACK ON PREVIOUS FIX]:\n"
268
+ f"The user reviewed the proposed fix and said:\n'{feedback}'\n\n"
269
+ f"Please update and refine the code correction to satisfy this feedback."
270
+ )
271
+
272
+ result.fix_generation_raw = self._step4_fix_generation(
273
+ refined_root_cause,
274
+ result.original_file_contents,
275
+ stream_callback,
276
+ )
277
+ result.fixed_files = parse_fixed_files_from_llm_response(
278
+ result.fix_generation_raw,
279
+ result.suspect_file_paths,
280
+ )
281
+ result.step_timings["4_refine"] = time.time() - t4
282
+ self._emit("4_refine", "complete",
283
+ f"Generated refined fixes for {len(result.fixed_files)} file(s) in "
284
+ f"{result.step_timings['4_refine']:.1f}s")
285
+
286
+ # ── Regenerate Diff & Explanation ─────────────────────────────
287
+ self._emit("5_diff", "running", "Generating updated diff and PR explanation...")
288
+ t5 = time.time()
289
+
290
+ result.diffs = generate_all_diffs(
291
+ result.original_file_contents, result.fixed_files
292
+ )
293
+ result.diff_formatted = format_diff_for_display(result.diffs)
294
+ result.diff_stats = get_diff_stats(result.diffs)
295
+
296
+ result.fix_explanation = self._step5_explanation(
297
+ result.bug_summary,
298
+ refined_root_cause,
299
+ result.diff_formatted,
300
+ stream_callback,
301
+ )
302
+ result.step_timings["5_diff_refined"] = time.time() - t5
303
+ self._emit("5_diff", "complete",
304
+ f"Updated PR explanation ready in {result.step_timings['5_diff_refined']:.1f}s")
305
+
306
+ except Exception as e:
307
+ logger.exception("FixFlow refinement failed")
308
+ step = self._current_step or "unknown"
309
+ result.step_errors[step] = str(e)
310
+ self._emit(step, "error", f"❌ Refinement failed: {e}")
311
+ raise
312
+
313
+ return result
314
+
315
  # ── Pipeline Steps ────────────────────────────────────────────────────────
316
 
317
  def _step1_issue_understanding(
 
454
  ]
455
  return self._llm_call(messages, stream_cb, temperature=0.3)
456
 
457
+ def _run_confidence_eval(self, analysis: str, stream_cb: Optional[Callable] = None) -> str:
458
  self._current_step = "6_confidence"
459
  prompt = CONFIDENCE_EVAL_PROMPT.format(analysis=analysis[:4000])
460
  messages = [
backend/api.py ADDED
@@ -0,0 +1,275 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import asyncio
2
+ import json
3
+ import logging
4
+ import time
5
+ import uuid
6
+ from threading import Thread
7
+ from typing import Dict, Any
8
+
9
+ from fastapi import FastAPI, HTTPException
10
+ from fastapi.middleware.cors import CORSMiddleware
11
+ from pydantic import BaseModel
12
+ from sse_starlette.sse import EventSourceResponse
13
+
14
+ from backend.agent import FixFlowAgent
15
+ from backend.config import GLM_MODEL, GLM_BASE_URL, GLM_API_KEY, GITHUB_TOKEN
16
+ from backend.github_client import GitHubClient
17
+ from backend.llm_client import GLMClient
18
+
19
+ logger = logging.getLogger(__name__)
20
+
21
+ app = FastAPI(title="FixFlow API")
22
+
23
+ app.add_middleware(
24
+ CORSMiddleware,
25
+ allow_origins=["*"],
26
+ allow_credentials=True,
27
+ allow_methods=["*"],
28
+ allow_headers=["*"],
29
+ )
30
+
31
+ SESSION_CACHE: Dict[str, Any] = {}
32
+
33
+ class AnalyzeRequest(BaseModel):
34
+ issue_url: str
35
+ repo_url: str
36
+ run_confidence: bool = True
37
+
38
+ @app.get("/api/analyze")
39
+ async def analyze_endpoint(issue_url: str, repo_url: str, run_confidence: bool = True):
40
+ if not GLM_API_KEY:
41
+ raise HTTPException(status_code=400, detail="Missing GLM API key in backend config")
42
+
43
+ session_id = str(uuid.uuid4())
44
+ queue = asyncio.Queue()
45
+
46
+ def sync_runner():
47
+ llm = GLMClient(
48
+ api_key=GLM_API_KEY,
49
+ base_url=GLM_BASE_URL,
50
+ model=GLM_MODEL,
51
+ )
52
+ gh = GitHubClient(token=GITHUB_TOKEN)
53
+ agent = FixFlowAgent(llm_client=llm, github_client=gh)
54
+
55
+ def on_status(step: str, status: str, message: str):
56
+ asyncio.run_coroutine_threadsafe(
57
+ queue.put({"type": "status", "data": {"step": step, "status": status, "message": message}}),
58
+ loop
59
+ )
60
+
61
+ def on_stream(chunk: str):
62
+ asyncio.run_coroutine_threadsafe(
63
+ queue.put({"type": "stream", "data": {"chunk": chunk}}),
64
+ loop
65
+ )
66
+
67
+ try:
68
+ result = agent.run(
69
+ issue_url=issue_url,
70
+ repo_url=repo_url,
71
+ on_status=on_status,
72
+ stream_callback=on_stream,
73
+ run_confidence_eval=run_confidence,
74
+ )
75
+
76
+ payload = {
77
+ "bug_summary": result.bug_summary,
78
+ "relevant_files_analysis": result.relevant_files_analysis,
79
+ "suspect_file_paths": result.suspect_file_paths,
80
+ "root_cause_analysis": result.root_cause_analysis,
81
+ "diff_formatted": result.diff_formatted,
82
+ "fix_explanation": result.fix_explanation,
83
+ "diff_stats": result.diff_stats,
84
+ "step_timings": result.step_timings,
85
+ "fixed_files": result.fixed_files,
86
+ "issue_title": result.issue_data.get("title", "Bug fix")
87
+ }
88
+ SESSION_CACHE[session_id] = result
89
+ asyncio.run_coroutine_threadsafe(
90
+ queue.put({"type": "done", "data": {"result": payload, "session_id": session_id}}),
91
+ loop
92
+ )
93
+ except Exception as e:
94
+ logger.exception("Agent run failed")
95
+ asyncio.run_coroutine_threadsafe(
96
+ queue.put({"type": "error", "data": {"error": str(e)}}),
97
+ loop
98
+ )
99
+ finally:
100
+ asyncio.run_coroutine_threadsafe(
101
+ queue.put({"type": "eof"}),
102
+ loop
103
+ )
104
+
105
+ loop = asyncio.get_running_loop()
106
+ thread = Thread(target=sync_runner)
107
+ thread.start()
108
+
109
+ async def event_generator():
110
+ while True:
111
+ try:
112
+ # Wait up to 15s before sending a heartbeat ping to keep connection alive
113
+ msg = await asyncio.wait_for(queue.get(), timeout=15.0)
114
+ if msg["type"] == "eof":
115
+ break
116
+ yield {
117
+ "event": msg["type"],
118
+ "data": json.dumps(msg["data"])
119
+ }
120
+ except asyncio.TimeoutError:
121
+ # Send a heartbeat comment to prevent browser SSE timeout
122
+ yield {"event": "heartbeat", "data": json.dumps({"alive": True})}
123
+
124
+ return EventSourceResponse(event_generator())
125
+
126
+
127
+ class PRRequest(BaseModel):
128
+ repo_url: str
129
+ title: str
130
+ body: str
131
+ fixed_files: Dict[str, str]
132
+
133
+ @app.post("/api/pr")
134
+ def create_pr(req: PRRequest):
135
+ if not GITHUB_TOKEN:
136
+ raise HTTPException(status_code=400, detail="Missing GITHUB_TOKEN in backend config for PR creation")
137
+
138
+ gh = GitHubClient(token=GITHUB_TOKEN)
139
+ branch_name = f"fixflow-patch-{int(time.time())}"
140
+ try:
141
+ url = gh.create_pull_request(
142
+ repo_url=req.repo_url,
143
+ branch_name=branch_name,
144
+ files_content=req.fixed_files,
145
+ title=req.title,
146
+ body=req.body
147
+ )
148
+ return {"url": url}
149
+ except Exception as e:
150
+ logger.exception("PR creation failed")
151
+ raise HTTPException(status_code=400, detail=str(e))
152
+
153
+ @app.get("/api/repo_info")
154
+ def get_repo_info(repo_url: str):
155
+ """
156
+ Fetch repository file tree and list open issues.
157
+ """
158
+ try:
159
+ gh = GitHubClient(token=GITHUB_TOKEN)
160
+ # Fetch repo tree (flat list)
161
+ tree = gh.fetch_repo_tree(repo_url)
162
+ # Fetch top 10 open issues
163
+ issues = gh.list_open_issues(repo_url, limit=10)
164
+
165
+ return {
166
+ "tree": tree,
167
+ "issues": issues,
168
+ "repo_url": repo_url
169
+ }
170
+ except Exception as e:
171
+ logger.exception("Repo info fetch failed")
172
+ raise HTTPException(status_code=400, detail=str(e))
173
+
174
+ raise HTTPException(status_code=400, detail=str(e))
175
+
176
+ @app.get("/api/file_content")
177
+ def get_file_content(repo_url: str, file_path: str):
178
+ """
179
+ Fetch the raw content of a specific file in the repository.
180
+ """
181
+ try:
182
+ gh = GitHubClient(token=GITHUB_TOKEN)
183
+ content = gh.fetch_file_content(repo_url, file_path)
184
+ return {"content": content, "path": file_path}
185
+ except Exception as e:
186
+ logger.exception("File content fetch failed")
187
+ raise HTTPException(status_code=400, detail=str(e))
188
+
189
+ @app.get("/api/refine")
190
+ async def refine_endpoint(session_id: str, feedback: str):
191
+ if not GLM_API_KEY:
192
+ raise HTTPException(status_code=400, detail="Missing GLM API key in backend config")
193
+
194
+ if session_id not in SESSION_CACHE:
195
+ raise HTTPException(status_code=404, detail="Session not found or expired")
196
+
197
+ queue = asyncio.Queue()
198
+
199
+ def sync_runner():
200
+ llm = GLMClient(
201
+ api_key=GLM_API_KEY,
202
+ base_url=GLM_BASE_URL,
203
+ model=GLM_MODEL,
204
+ )
205
+ gh = GitHubClient(token=GITHUB_TOKEN)
206
+ agent = FixFlowAgent(llm_client=llm, github_client=gh)
207
+ previous_result = SESSION_CACHE[session_id]
208
+
209
+ def on_status(step: str, status: str, message: str):
210
+ asyncio.run_coroutine_threadsafe(
211
+ queue.put({"type": "status", "data": {"step": step, "status": status, "message": message}}),
212
+ loop
213
+ )
214
+
215
+ def on_stream(chunk: str):
216
+ asyncio.run_coroutine_threadsafe(
217
+ queue.put({"type": "stream", "data": {"chunk": chunk}}),
218
+ loop
219
+ )
220
+
221
+ try:
222
+ result = agent.refine_fix(
223
+ feedback=feedback,
224
+ result=previous_result,
225
+ on_status=on_status,
226
+ stream_callback=on_stream,
227
+ )
228
+
229
+ payload = {
230
+ "bug_summary": result.bug_summary,
231
+ "relevant_files_analysis": result.relevant_files_analysis,
232
+ "suspect_file_paths": result.suspect_file_paths,
233
+ "root_cause_analysis": result.root_cause_analysis,
234
+ "diff_formatted": result.diff_formatted,
235
+ "fix_explanation": result.fix_explanation,
236
+ "diff_stats": result.diff_stats,
237
+ "step_timings": result.step_timings,
238
+ "fixed_files": result.fixed_files,
239
+ "issue_title": result.issue_data.get("title", "Bug fix")
240
+ }
241
+ SESSION_CACHE[session_id] = result
242
+ asyncio.run_coroutine_threadsafe(
243
+ queue.put({"type": "done", "data": {"result": payload, "session_id": session_id}}),
244
+ loop
245
+ )
246
+ except Exception as e:
247
+ logger.exception("Agent run failed")
248
+ asyncio.run_coroutine_threadsafe(
249
+ queue.put({"type": "error", "data": {"error": str(e)}}),
250
+ loop
251
+ )
252
+ finally:
253
+ asyncio.run_coroutine_threadsafe(
254
+ queue.put({"type": "eof"}),
255
+ loop
256
+ )
257
+
258
+ loop = asyncio.get_running_loop()
259
+ thread = Thread(target=sync_runner)
260
+ thread.start()
261
+
262
+ async def event_generator():
263
+ while True:
264
+ try:
265
+ msg = await asyncio.wait_for(queue.get(), timeout=15.0)
266
+ if msg["type"] == "eof":
267
+ break
268
+ yield {
269
+ "event": msg["type"],
270
+ "data": json.dumps(msg["data"])
271
+ }
272
+ except asyncio.TimeoutError:
273
+ yield {"event": "heartbeat", "data": json.dumps({"alive": True})}
274
+
275
+ return EventSourceResponse(event_generator())
backend/github_client.py CHANGED
@@ -120,6 +120,41 @@ class GitHubClient:
120
  "repo_name": repo_name,
121
  }
122
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
  # ── Repo Tree ─────────────────────────────────────────────────────────────
124
 
125
  def fetch_repo_tree(
 
120
  "repo_name": repo_name,
121
  }
122
 
123
+ def list_open_issues(self, repo_url: str, limit: int = 20) -> List[Dict]:
124
+ """
125
+ List open issues for a repository.
126
+ Returns a list of structured dicts: {title, number, url, author, created_at, body_snippet}
127
+ """
128
+ owner, repo_name = parse_repo_url(repo_url)
129
+ logger.info("Listing open issues for %s/%s", owner, repo_name)
130
+
131
+ try:
132
+ repo = self._gh.get_repo(f"{owner}/{repo_name}")
133
+ # state='open' by default
134
+ issues = repo.get_issues(state='open', sort='updated', direction='desc')
135
+
136
+ result = []
137
+ for issue in issues:
138
+ # Skip Pull Requests (PyGithub get_issues() returns both)
139
+ if issue.pull_request:
140
+ continue
141
+
142
+ result.append({
143
+ "title": issue.title,
144
+ "number": issue.number,
145
+ "url": issue.html_url,
146
+ "author": issue.user.login if issue.user else "unknown",
147
+ "created_at": str(issue.created_at),
148
+ "body_snippet": (issue.body[:200] + "...") if issue.body else "",
149
+ })
150
+ if len(result) >= limit:
151
+ break
152
+ return result
153
+ except GithubException as e:
154
+ raise RuntimeError(
155
+ f"Failed to list issues: {e.data.get('message', str(e))}"
156
+ ) from e
157
+
158
  # ── Repo Tree ─────────────────────────────────────────────────────────────
159
 
160
  def fetch_repo_tree(
backend/llm_client.py CHANGED
@@ -1,14 +1,19 @@
1
  """
2
  LLM Client for GLM 5.1 via Z.ai API (OpenAI-compatible endpoint).
 
3
  """
4
  import time
5
  import logging
 
6
  from typing import Iterator, List, Dict, Any, Optional
7
  import openai
8
  from backend.config import GLM_API_KEY, GLM_BASE_URL, GLM_MODEL, LOG_LLM_CALLS
9
 
10
  logger = logging.getLogger(__name__)
11
 
 
 
 
12
 
13
  class GLMClient:
14
  """OpenAI-compatible wrapper for Z.ai's GLM models."""
@@ -36,13 +41,19 @@ class GLMClient:
36
  )
37
  return self._client
38
 
 
 
 
 
 
 
39
  def chat(
40
  self,
41
  messages: List[Dict[str, str]],
42
  temperature: float = 0.3,
43
  max_tokens: int = 4096,
44
  ) -> str:
45
- """Synchronous chat completion. Returns the full response string."""
46
  client = self._get_client()
47
  start = time.time()
48
 
@@ -52,19 +63,36 @@ class GLMClient:
52
  self.model, len(messages), temperature,
53
  )
54
 
55
- response = client.chat.completions.create(
56
- model=self.model,
57
- messages=messages,
58
- temperature=temperature,
59
- max_tokens=max_tokens,
60
- )
61
- content = response.choices[0].message.content or ""
62
- elapsed = time.time() - start
 
 
 
63
 
64
- if LOG_LLM_CALLS:
65
- logger.info("[GLM] completed in %.2fs | output_chars=%d", elapsed, len(content))
 
 
66
 
67
- return content
 
 
 
 
 
 
 
 
 
 
 
 
68
 
69
  def chat_stream(
70
  self,
@@ -72,7 +100,7 @@ class GLMClient:
72
  temperature: float = 0.3,
73
  max_tokens: int = 4096,
74
  ) -> Iterator[str]:
75
- """Streaming chat completion. Yields text chunks as they arrive."""
76
  client = self._get_client()
77
 
78
  if LOG_LLM_CALLS:
@@ -81,19 +109,38 @@ class GLMClient:
81
  self.model, len(messages),
82
  )
83
 
84
- response = client.chat.completions.create(
85
- model=self.model,
86
- messages=messages,
87
- temperature=temperature,
88
- max_tokens=max_tokens,
89
- stream=True,
90
- )
91
- for chunk in response:
92
- delta = chunk.choices[0].delta
93
- if delta and delta.content:
94
- yield delta.content
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
 
96
  def update_api_key(self, api_key: str) -> None:
97
  """Allow hot-swapping the API key (e.g. from Streamlit sidebar)."""
98
  self.api_key = api_key
99
  self._client = None # Force re-initialization
 
 
1
  """
2
  LLM Client for GLM 5.1 via Z.ai API (OpenAI-compatible endpoint).
3
+ Includes automatic retry with exponential backoff for rate-limit (429) errors.
4
  """
5
  import time
6
  import logging
7
+ import random
8
  from typing import Iterator, List, Dict, Any, Optional
9
  import openai
10
  from backend.config import GLM_API_KEY, GLM_BASE_URL, GLM_MODEL, LOG_LLM_CALLS
11
 
12
  logger = logging.getLogger(__name__)
13
 
14
+ MAX_RETRIES = 5
15
+ INITIAL_BACKOFF = 5 # seconds
16
+
17
 
18
  class GLMClient:
19
  """OpenAI-compatible wrapper for Z.ai's GLM models."""
 
41
  )
42
  return self._client
43
 
44
+ def _backoff_wait(self, attempt: int) -> None:
45
+ """Exponential backoff with jitter. Waits and logs the wait time."""
46
+ wait = INITIAL_BACKOFF * (2 ** attempt) + random.uniform(0, 2)
47
+ logger.warning("[GLM] Rate limited (429). Retrying in %.1fs (attempt %d/%d)...", wait, attempt + 1, MAX_RETRIES)
48
+ time.sleep(wait)
49
+
50
  def chat(
51
  self,
52
  messages: List[Dict[str, str]],
53
  temperature: float = 0.3,
54
  max_tokens: int = 4096,
55
  ) -> str:
56
+ """Synchronous chat completion with automatic retry on 429."""
57
  client = self._get_client()
58
  start = time.time()
59
 
 
63
  self.model, len(messages), temperature,
64
  )
65
 
66
+ last_error = None
67
+ for attempt in range(MAX_RETRIES):
68
+ try:
69
+ response = client.chat.completions.create(
70
+ model=self.model,
71
+ messages=messages,
72
+ temperature=temperature,
73
+ max_tokens=max_tokens,
74
+ )
75
+ content = response.choices[0].message.content or ""
76
+ elapsed = time.time() - start
77
 
78
+ if LOG_LLM_CALLS:
79
+ logger.info("[GLM] completed in %.2fs | output_chars=%d", elapsed, len(content))
80
+
81
+ return content
82
 
83
+ except openai.RateLimitError as e:
84
+ last_error = e
85
+ if attempt < MAX_RETRIES - 1:
86
+ self._backoff_wait(attempt)
87
+ else:
88
+ raise RuntimeError(
89
+ f"GLM API rate limit exceeded after {MAX_RETRIES} retries. "
90
+ f"Please wait a moment and try again. Detail: {e}"
91
+ ) from e
92
+ except openai.APIError as e:
93
+ raise RuntimeError(f"GLM API error: {e}") from e
94
+
95
+ raise RuntimeError(f"GLM request failed after {MAX_RETRIES} attempts: {last_error}")
96
 
97
  def chat_stream(
98
  self,
 
100
  temperature: float = 0.3,
101
  max_tokens: int = 4096,
102
  ) -> Iterator[str]:
103
+ """Streaming chat completion with automatic retry on 429."""
104
  client = self._get_client()
105
 
106
  if LOG_LLM_CALLS:
 
109
  self.model, len(messages),
110
  )
111
 
112
+ last_error = None
113
+ for attempt in range(MAX_RETRIES):
114
+ try:
115
+ response = client.chat.completions.create(
116
+ model=self.model,
117
+ messages=messages,
118
+ temperature=temperature,
119
+ max_tokens=max_tokens,
120
+ stream=True,
121
+ )
122
+ for chunk in response:
123
+ delta = chunk.choices[0].delta
124
+ if delta and delta.content:
125
+ yield delta.content
126
+ return # Completed successfully
127
+
128
+ except openai.RateLimitError as e:
129
+ last_error = e
130
+ if attempt < MAX_RETRIES - 1:
131
+ self._backoff_wait(attempt)
132
+ else:
133
+ raise RuntimeError(
134
+ f"GLM API rate limit exceeded after {MAX_RETRIES} retries. "
135
+ f"Please wait a moment and try again. Detail: {e}"
136
+ ) from e
137
+ except openai.APIError as e:
138
+ raise RuntimeError(f"GLM API error: {e}") from e
139
+
140
+ raise RuntimeError(f"GLM stream failed after {MAX_RETRIES} attempts: {last_error}")
141
 
142
  def update_api_key(self, api_key: str) -> None:
143
  """Allow hot-swapping the API key (e.g. from Streamlit sidebar)."""
144
  self.api_key = api_key
145
  self._client = None # Force re-initialization
146
+
frontend/.gitignore ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2
+
3
+ # dependencies
4
+ /node_modules
5
+ /.pnp
6
+ .pnp.*
7
+ .yarn/*
8
+ !.yarn/patches
9
+ !.yarn/plugins
10
+ !.yarn/releases
11
+ !.yarn/versions
12
+
13
+ # testing
14
+ /coverage
15
+
16
+ # next.js
17
+ /.next/
18
+ /out/
19
+
20
+ # production
21
+ /build
22
+
23
+ # misc
24
+ .DS_Store
25
+ *.pem
26
+
27
+ # debug
28
+ npm-debug.log*
29
+ yarn-debug.log*
30
+ yarn-error.log*
31
+ .pnpm-debug.log*
32
+
33
+ # env files (can opt-in for committing if needed)
34
+ .env*
35
+
36
+ # vercel
37
+ .vercel
38
+
39
+ # typescript
40
+ *.tsbuildinfo
41
+ next-env.d.ts
frontend/AGENTS.md ADDED
@@ -0,0 +1,5 @@
 
 
 
 
 
 
1
+ <!-- BEGIN:nextjs-agent-rules -->
2
+ # This is NOT the Next.js you know
3
+
4
+ This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
5
+ <!-- END:nextjs-agent-rules -->
frontend/CLAUDE.md ADDED
@@ -0,0 +1 @@
 
 
1
+ @AGENTS.md
frontend/README.md ADDED
@@ -0,0 +1,36 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
2
+
3
+ ## Getting Started
4
+
5
+ First, run the development server:
6
+
7
+ ```bash
8
+ npm run dev
9
+ # or
10
+ yarn dev
11
+ # or
12
+ pnpm dev
13
+ # or
14
+ bun dev
15
+ ```
16
+
17
+ Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
18
+
19
+ You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
20
+
21
+ This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
22
+
23
+ ## Learn More
24
+
25
+ To learn more about Next.js, take a look at the following resources:
26
+
27
+ - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
28
+ - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
29
+
30
+ You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
31
+
32
+ ## Deploy on Vercel
33
+
34
+ The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
35
+
36
+ Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
frontend/eslint.config.mjs ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { defineConfig, globalIgnores } from "eslint/config";
2
+ import nextVitals from "eslint-config-next/core-web-vitals";
3
+ import nextTs from "eslint-config-next/typescript";
4
+
5
+ const eslintConfig = defineConfig([
6
+ ...nextVitals,
7
+ ...nextTs,
8
+ // Override default ignores of eslint-config-next.
9
+ globalIgnores([
10
+ // Default ignores of eslint-config-next:
11
+ ".next/**",
12
+ "out/**",
13
+ "build/**",
14
+ "next-env.d.ts",
15
+ ]),
16
+ ]);
17
+
18
+ export default eslintConfig;
frontend/next.config.ts ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import type { NextConfig } from "next";
2
+
3
+ const nextConfig: NextConfig = {
4
+ /* config options here */
5
+ };
6
+
7
+ export default nextConfig;
frontend/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
frontend/package.json ADDED
@@ -0,0 +1,24 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "frontend",
3
+ "version": "0.1.0",
4
+ "private": true,
5
+ "scripts": {
6
+ "dev": "next dev",
7
+ "build": "next build",
8
+ "start": "next start",
9
+ "lint": "eslint"
10
+ },
11
+ "dependencies": {
12
+ "next": "16.2.2",
13
+ "react": "19.2.4",
14
+ "react-dom": "19.2.4"
15
+ },
16
+ "devDependencies": {
17
+ "@types/node": "^20",
18
+ "@types/react": "^19",
19
+ "@types/react-dom": "^19",
20
+ "eslint": "^9",
21
+ "eslint-config-next": "16.2.2",
22
+ "typescript": "^5"
23
+ }
24
+ }
frontend/public/file.svg ADDED
frontend/public/globe.svg ADDED
frontend/public/next.svg ADDED
frontend/public/vercel.svg ADDED
frontend/public/window.svg ADDED
frontend/src/app/[owner]/[repo]/page.tsx ADDED
@@ -0,0 +1,421 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+ import { useParams, useRouter } from 'next/navigation';
5
+
6
+ type UI_STATE = 'LOADING_REPO' | 'REPO_DASHBOARD';
7
+
8
+ interface GitHubIssue {
9
+ title: string;
10
+ number: number;
11
+ url: string;
12
+ author: string;
13
+ created_at: string;
14
+ body_snippet: string;
15
+ }
16
+
17
+ interface RepoFile {
18
+ path: string;
19
+ size: number;
20
+ type: string;
21
+ }
22
+
23
+ export default function RepoDashboard() {
24
+ const params = useParams();
25
+ const router = useRouter();
26
+ const owner = params.owner as string;
27
+ const repo = params.repo as string;
28
+ const repoUrl = `https://github.com/${owner}/${repo}`;
29
+
30
+ const [uiState, setUiState] = useState<UI_STATE>('LOADING_REPO');
31
+ const [repoInfo, setRepoInfo] = useState<{ tree: RepoFile[], issues: GitHubIssue[] } | null>(null);
32
+ const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
33
+ const [selectedFileContent, setSelectedFileContent] = useState<string>("");
34
+ const [isLoadingFile, setIsLoadingFile] = useState(false);
35
+ const [error, setError] = useState("");
36
+
37
+ const [issueUrl, setIssueUrl] = useState("");
38
+ const [runConfidence] = useState(true);
39
+ const [running, setRunning] = useState(false);
40
+ const [statusName, setStatusName] = useState("");
41
+ const [statusMessage, setStatusMessage] = useState("");
42
+ const [streamChunks, setStreamChunks] = useState<string[]>([]);
43
+ const [result, setResult] = useState<any>(null);
44
+ const [analyzingFiles, setAnalyzingFiles] = useState<string[]>([]);
45
+ const [terminalExpanded, setTerminalExpanded] = useState(true);
46
+ const [completedSteps, setCompletedSteps] = useState<{step: string, message: string, status: string}[]>([]);
47
+ const [fixedFileView, setFixedFileView] = useState<{path: string, content: string} | null>(null);
48
+ const [sessionId, setSessionId] = useState("");
49
+ const [feedback, setFeedback] = useState("");
50
+
51
+ const logsEndRef = useRef<HTMLDivElement>(null);
52
+
53
+ // Auto-load repo on mount
54
+ useEffect(() => {
55
+ const loadRepo = async () => {
56
+ try {
57
+ const res = await fetch(`http://127.0.0.1:8000/api/repo_info?repo_url=${encodeURIComponent(repoUrl)}`);
58
+ if (!res.ok) {
59
+ const data = await res.json();
60
+ throw new Error(data.detail || "Failed to fetch repository info");
61
+ }
62
+ const data = await res.json();
63
+ setRepoInfo(data);
64
+ setUiState('REPO_DASHBOARD');
65
+ if (data.tree && data.tree.length > 0) {
66
+ handleFileClick(data.tree[0].path, repoUrl);
67
+ }
68
+ } catch (err: any) {
69
+ setError(err.message);
70
+ }
71
+ };
72
+ loadRepo();
73
+ }, [owner, repo]);
74
+
75
+ useEffect(() => {
76
+ if (logsEndRef.current) {
77
+ logsEndRef.current.scrollIntoView({ behavior: "smooth" });
78
+ }
79
+ }, [streamChunks, statusMessage, running]);
80
+
81
+ useEffect(() => {
82
+ if (statusMessage) {
83
+ const pathRegex = /([a-zA-Z0-9._\-/]+\.(py|js|tsx|ts|html|css|json|md))/g;
84
+ const matches = statusMessage.match(pathRegex);
85
+ if (matches) {
86
+ setAnalyzingFiles(prev => [...new Set([...prev, ...matches])]);
87
+ const validPath = matches.find(p => repoInfo?.tree.some(f => f.path === p));
88
+ if (validPath && validPath !== selectedFilePath) {
89
+ handleFileClick(validPath, repoUrl);
90
+ }
91
+ }
92
+ }
93
+ }, [statusMessage]);
94
+
95
+ const handleFileClick = async (path: string, url?: string) => {
96
+ const repoUrlToUse = url || repoUrl;
97
+ setSelectedFilePath(path);
98
+ setIsLoadingFile(true);
99
+ try {
100
+ const res = await fetch(`http://127.0.0.1:8000/api/file_content?repo_url=${encodeURIComponent(repoUrlToUse)}&file_path=${encodeURIComponent(path)}`);
101
+ if (!res.ok) throw new Error("Failed to fetch file content");
102
+ const data = await res.json();
103
+ setSelectedFileContent(data.content);
104
+ } catch {
105
+ setSelectedFileContent("// Error loading file content...");
106
+ } finally {
107
+ setIsLoadingFile(false);
108
+ }
109
+ };
110
+
111
+ const handleAnalyze = (selectedIssueUrl?: string) => {
112
+ const finalIssueUrl = selectedIssueUrl || issueUrl;
113
+ if (!finalIssueUrl) { setError("Please provide an Issue URL"); return; }
114
+
115
+ setRunning(true);
116
+ setError("");
117
+ setResult(null);
118
+ setStreamChunks([]);
119
+ setAnalyzingFiles([]);
120
+ setCompletedSteps([]);
121
+ setFixedFileView(null);
122
+ setTerminalExpanded(true);
123
+ setStatusName("Starting");
124
+ setStatusMessage("Connecting to FixFlow API...");
125
+
126
+ const params = new URLSearchParams({ issue_url: finalIssueUrl, repo_url: repoUrl, run_confidence: runConfidence.toString() });
127
+ const eventSource = new EventSource(`http://127.0.0.1:8000/api/analyze?${params.toString()}`);
128
+ setupEventSource(eventSource);
129
+ };
130
+
131
+ const handleRefine = () => {
132
+ if (!feedback || !sessionId) return;
133
+ setRunning(true);
134
+ setError("");
135
+ setStreamChunks([]);
136
+ setAnalyzingFiles([]);
137
+ setCompletedSteps([]);
138
+ setFixedFileView(null);
139
+ setTerminalExpanded(true);
140
+ setStatusName("Refining");
141
+ setStatusMessage("Sending feedback to FixFlow...");
142
+ const params = new URLSearchParams({ session_id: sessionId, feedback });
143
+ setFeedback("");
144
+ const eventSource = new EventSource(`http://127.0.0.1:8000/api/refine?${params.toString()}`);
145
+ setupEventSource(eventSource);
146
+ };
147
+
148
+ const setupEventSource = (eventSource: EventSource) => {
149
+ eventSource.addEventListener("status", (e: Event) => {
150
+ const data = JSON.parse((e as MessageEvent).data);
151
+ const { step, status, message } = data;
152
+ setStatusName(step);
153
+ setStatusMessage(message);
154
+ if (status === "complete" || status === "error") {
155
+ setCompletedSteps(prev => [...prev, { step, status, message }]);
156
+ }
157
+ });
158
+ eventSource.addEventListener("stream", (e: Event) => {
159
+ const data = JSON.parse((e as MessageEvent).data);
160
+ setStreamChunks(prev => [...prev, data.chunk]);
161
+ });
162
+ eventSource.addEventListener("done", (e: Event) => {
163
+ const data = JSON.parse((e as MessageEvent).data);
164
+ const r = data.result;
165
+ setResult(r);
166
+ if (data.session_id) setSessionId(data.session_id);
167
+ if (r?.fixed_files) {
168
+ const paths = Object.keys(r.fixed_files);
169
+ if (paths.length > 0) {
170
+ setFixedFileView({ path: paths[0], content: r.fixed_files[paths[0]] });
171
+ setSelectedFilePath(paths[0]);
172
+ }
173
+ }
174
+ });
175
+ eventSource.addEventListener("error", (e: Event) => {
176
+ const data = JSON.parse((e as MessageEvent).data);
177
+ setError(data.error || "Unknown stream error");
178
+ setRunning(false);
179
+ eventSource.close();
180
+ });
181
+ eventSource.addEventListener("eof", () => {
182
+ setRunning(false);
183
+ setStatusName("Done");
184
+ setStatusMessage("Fix generated — review the highlighted file in the editor.");
185
+ setCompletedSteps(prev => [...prev, { step: "done", status: "complete", message: "Fix generated — review highlighted file!" }]);
186
+ eventSource.close();
187
+ });
188
+ eventSource.addEventListener("heartbeat", () => {});
189
+ eventSource.onerror = (e) => console.error("SSE Error:", e);
190
+ };
191
+
192
+ const handleOpenPR = async () => {
193
+ if (!result) return;
194
+ try {
195
+ const res = await fetch("http://127.0.0.1:8000/api/pr", {
196
+ method: "POST",
197
+ headers: { "Content-Type": "application/json" },
198
+ body: JSON.stringify({
199
+ repo_url: repoUrl,
200
+ title: result.issue_title,
201
+ body: result.fix_explanation + "\n\n---\n*Generated autonomously by FixFlow*",
202
+ fixed_files: result.fixed_files
203
+ })
204
+ });
205
+ const data = await res.json();
206
+ if (!res.ok) throw new Error(data.detail || "Failed to create PR");
207
+ window.open(data.url, "_blank");
208
+ } catch (err: any) { alert("Error: " + err.message); }
209
+ };
210
+
211
+ // ── Loading Screen ────────────────────────────────────────────────────────
212
+ if (uiState === 'LOADING_REPO') {
213
+ return (
214
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
215
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 24px', borderBottom: '1px solid var(--border-color)', flexShrink: 0 }}>
216
+ <span style={{ fontSize: '1.4rem', cursor: 'pointer' }} onClick={() => router.push('/')}>🔧</span>
217
+ <span style={{ fontWeight: 800, fontSize: '1.2rem', background: 'linear-gradient(135deg, var(--primary), var(--secondary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>FixFlow</span>
218
+ <span style={{ color: 'var(--text-muted)', fontSize: '0.8rem', fontFamily: 'monospace' }}>/ {owner} / {repo}</span>
219
+ </div>
220
+ <div style={{ flex: 1, display: 'flex', alignItems: 'center', justifyContent: 'center', flexDirection: 'column', gap: 16 }}>
221
+ <div className="loading-spinner" style={{ width: 36, height: 36 }} />
222
+ <p style={{ color: 'var(--text-muted)', fontSize: '0.9rem' }}>Cloning <strong style={{ color: 'var(--text-main)' }}>{owner}/{repo}</strong>...</p>
223
+ {error && <div style={{ color: 'var(--error)', fontSize: '0.85rem' }}>❌ {error}</div>}
224
+ </div>
225
+ </div>
226
+ );
227
+ }
228
+
229
+ // ── IDE Dashboard ─────────────────────────────────────────────────────────
230
+ if (!repoInfo) return null;
231
+
232
+ return (
233
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
234
+ {/* Top Bar */}
235
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 24px', borderBottom: '1px solid var(--border-color)', flexShrink: 0 }}>
236
+ <span style={{ fontSize: '1.4rem', cursor: 'pointer' }} onClick={() => router.push('/')}>🔧</span>
237
+ <span style={{ fontWeight: 800, fontSize: '1.2rem', background: 'linear-gradient(135deg, var(--primary), var(--secondary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>FixFlow</span>
238
+ <span style={{ color: 'var(--border-color)' }}>/</span>
239
+ <a href={repoUrl} target="_blank" rel="noreferrer" style={{ color: 'var(--text-muted)', fontSize: '0.85rem', fontFamily: 'monospace', textDecoration: 'none' }}>
240
+ {owner}/<strong style={{ color: 'var(--text-main)' }}>{repo}</strong>
241
+ </a>
242
+ <span style={{ marginLeft: 'auto', fontSize: '0.75rem', color: 'var(--text-muted)' }}>{repoInfo.tree.length} files</span>
243
+ </div>
244
+
245
+ {/* Main IDE */}
246
+ <div style={{ flex: 1, padding: '16px', display: 'flex', overflow: 'hidden' }}>
247
+ <div style={{ display: 'grid', gridTemplateColumns: '240px 1fr 300px', gap: 16, flex: 1, minHeight: 0 }}>
248
+
249
+ {/* Left: Explorer */}
250
+ <div className="glass-panel" style={{ padding: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
251
+ <h3 style={{ marginBottom: 12, fontSize: '0.8rem', color: 'var(--text-main)', opacity: 0.6, letterSpacing: '0.1em' }}>📁 EXPLORER</h3>
252
+ <div style={{ flex: 1, overflowY: 'auto', fontSize: '0.78rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
253
+ {repoInfo.tree.map((file, i) => {
254
+ const isAnalyzing = analyzingFiles.includes(file.path);
255
+ return (
256
+ <div
257
+ key={i}
258
+ onClick={() => handleFileClick(file.path)}
259
+ className={isAnalyzing ? 'analyzing-glow' : ''}
260
+ style={{
261
+ padding: '5px 8px',
262
+ cursor: 'pointer',
263
+ borderRadius: '4px',
264
+ marginBottom: '1px',
265
+ backgroundColor: selectedFilePath === file.path ? 'rgba(139, 92, 246, 0.15)' : 'transparent',
266
+ color: selectedFilePath === file.path ? 'var(--primary)' : 'inherit',
267
+ whiteSpace: 'nowrap',
268
+ overflow: 'hidden',
269
+ textOverflow: 'ellipsis',
270
+ transition: 'all 0.2s ease'
271
+ }}
272
+ >
273
+ <span style={{ marginRight: 6 }}>{isAnalyzing ? '🧠' : '📄'}</span>
274
+ {file.path}
275
+ </div>
276
+ );
277
+ })}
278
+ </div>
279
+ </div>
280
+
281
+ {/* Center: Editor + Terminal Drawer */}
282
+ <div style={{ display: 'flex', flexDirection: 'column', minHeight: 0 }}>
283
+ <div className="terminal-window" style={{ flex: 1, display: 'flex', flexDirection: 'column', borderBottomLeftRadius: 0, borderBottomRightRadius: 0, minHeight: 0 }}>
284
+ <div className="terminal-header">
285
+ <div className="terminal-dot dot-red"></div>
286
+ <div className="terminal-dot dot-yellow"></div>
287
+ <div className="terminal-dot dot-green"></div>
288
+ <div style={{ marginLeft: '12px', fontSize: '0.75rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
289
+ {running && (statusName === '4_fix' || statusName === '4_refine') ? '🔧 Generating Fix...' : selectedFilePath || 'Editor'}
290
+ </div>
291
+ </div>
292
+ <div style={{ flex: 1, overflow: 'auto', backgroundColor: '#0d0d12' }}>
293
+ {fixedFileView && !running ? (
294
+ <pre style={{ margin: 0, padding: '20px', fontSize: '0.83rem', fontFamily: 'monospace', color: '#a3be8c', lineHeight: '1.6' }}>
295
+ {fixedFileView.content}
296
+ </pre>
297
+ ) : running && (statusName === '4_fix' || statusName === '4_refine') && streamChunks.length > 0 ? (
298
+ <pre style={{ margin: 0, padding: '20px', fontSize: '0.83rem', fontFamily: 'monospace', color: '#a3be8c', lineHeight: '1.6' }}>
299
+ {streamChunks.join('')}
300
+ <span style={{ borderRight: '2px solid var(--primary)' }}> </span>
301
+ <div ref={logsEndRef} />
302
+ </pre>
303
+ ) : (
304
+ <pre style={{ margin: 0, padding: '20px', fontSize: '0.83rem', fontFamily: 'monospace', color: '#d1d5db', lineHeight: '1.6' }}>
305
+ {selectedFileContent || '// Select a file to browse source code'}
306
+ </pre>
307
+ )}
308
+ </div>
309
+ </div>
310
+
311
+ {/* Terminal Drawer */}
312
+ <div className="terminal-drawer" style={{ height: terminalExpanded ? '200px' : '40px', borderRadius: '0 0 12px 12px' }}>
313
+ <div className="terminal-drawer-header" onClick={() => setTerminalExpanded(!terminalExpanded)}>
314
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
315
+ {running && <span className="loading-spinner" style={{ width: 12, height: 12 }} />}
316
+ <span style={{ color: running ? 'var(--primary)' : 'var(--text-muted)' }}>
317
+ {statusName ? `AGENT — [${statusName.toUpperCase()}]` : 'FIXFLOW AGENT CONSOLE'}
318
+ </span>
319
+ </div>
320
+ <span>{terminalExpanded ? '▼' : '▲ Show Progress'}</span>
321
+ </div>
322
+ <div className="terminal-drawer-body" style={{ padding: '10px 16px' }}>
323
+ {completedSteps.map((s, i) => (
324
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 6, fontSize: '0.78rem' }}>
325
+ <span style={{ color: s.status === 'error' ? 'var(--error)' : 'var(--success)', flexShrink: 0 }}>
326
+ {s.status === 'error' ? '✗' : '✓'}
327
+ </span>
328
+ <span style={{ color: 'var(--text-muted)' }}>{s.message}</span>
329
+ </div>
330
+ ))}
331
+ {running && statusMessage && (
332
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: '0.78rem' }}>
333
+ <span className="loading-spinner" style={{ width: 10, height: 10, flexShrink: 0 }} />
334
+ <span style={{ color: 'var(--primary)' }}>{statusMessage}</span>
335
+ </div>
336
+ )}
337
+ <div ref={logsEndRef} />
338
+ </div>
339
+ </div>
340
+ </div>
341
+
342
+ {/* Right: Discovery or Result */}
343
+ <div className="glass-panel" style={{ padding: 16, display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
344
+ {result && !running ? (
345
+ <>
346
+ <h3 style={{ marginBottom: 6, fontSize: '0.85rem', color: 'var(--success)' }}>✅ FIX READY</h3>
347
+ <p style={{ fontSize: '0.72rem', color: 'var(--text-muted)', marginBottom: 12, lineHeight: 1.5 }}>
348
+ {result.bug_summary?.slice(0, 150)}...
349
+ </p>
350
+ <div style={{ marginBottom: 12 }}>
351
+ <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)', marginBottom: 6, letterSpacing: '0.1em' }}>CHANGED FILES:</div>
352
+ {Object.keys(result.fixed_files || {}).map((path) => (
353
+ <div
354
+ key={path}
355
+ onClick={() => setFixedFileView({ path, content: result.fixed_files[path] })}
356
+ style={{
357
+ padding: '6px 10px', borderRadius: 6, marginBottom: 4, cursor: 'pointer',
358
+ fontSize: '0.72rem', fontFamily: 'monospace',
359
+ backgroundColor: fixedFileView?.path === path ? 'rgba(163, 190, 140, 0.15)' : 'rgba(255,255,255,0.04)',
360
+ color: fixedFileView?.path === path ? '#a3be8c' : 'var(--text-muted)',
361
+ border: fixedFileView?.path === path ? '1px solid rgba(163,190,140,0.3)' : '1px solid transparent',
362
+ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
363
+ }}
364
+ >
365
+ 📝 {path}
366
+ </div>
367
+ ))}
368
+ </div>
369
+ <div style={{ flex: 1 }} />
370
+ <button type="button" className="glow-btn" style={{ width: '100%', marginBottom: 8, padding: '10px', fontSize: '0.82rem' }} onClick={handleOpenPR}>
371
+ 🚀 Open Pull Request
372
+ </button>
373
+ <div style={{ display: 'flex', gap: 8, marginBottom: 8 }}>
374
+ <input className="input-field" style={{ flex: 1, padding: '7px 10px', fontSize: '0.75rem' }}
375
+ placeholder="Refine the fix..." value={feedback}
376
+ onChange={(e) => setFeedback(e.target.value)}
377
+ onKeyDown={(e) => e.key === 'Enter' && handleRefine()} />
378
+ <button type="button" className="glow-btn" style={{ padding: '8px 12px', fontSize: '0.8rem' }} onClick={handleRefine} disabled={!feedback}>↩</button>
379
+ </div>
380
+ <button type="button" style={{ background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '0.72rem' }}
381
+ onClick={() => { setResult(null); setFixedFileView(null); setCompletedSteps([]); }}>
382
+ ← Pick another issue
383
+ </button>
384
+ </>
385
+ ) : (
386
+ <>
387
+ <h3 style={{ marginBottom: 12, fontSize: '0.8rem', color: 'var(--text-main)', opacity: 0.6, letterSpacing: '0.1em' }}>🐛 ISSUES</h3>
388
+ <div style={{ flex: 1, overflowY: 'auto' }}>
389
+ {repoInfo.issues.length === 0 ? (
390
+ <p style={{ fontSize: '0.8rem', color: 'var(--text-muted)', textAlign: 'center', paddingTop: 20 }}>No open issues found.</p>
391
+ ) : repoInfo.issues.map((issue) => (
392
+ <div
393
+ key={issue.number}
394
+ className="step-card"
395
+ style={{ cursor: running ? 'default' : 'pointer', padding: '10px', marginBottom: 8, opacity: running ? 0.5 : 1 }}
396
+ onClick={() => !running && handleAnalyze(issue.url)}
397
+ >
398
+ <div style={{ fontWeight: 600, fontSize: '0.78rem', marginBottom: 2 }}>#{issue.number} {issue.title}</div>
399
+ <div style={{ fontSize: '0.68rem', color: 'var(--text-muted)' }}>@{issue.author}</div>
400
+ </div>
401
+ ))}
402
+ </div>
403
+ {!running && (
404
+ <div style={{ borderTop: '1px solid var(--border-color)', paddingTop: 12, marginTop: 8 }}>
405
+ <input className="input-field" style={{ padding: '8px 10px', fontSize: '0.78rem', marginBottom: 8 }}
406
+ placeholder="Paste issue URL..." value={issueUrl} onChange={(e) => setIssueUrl(e.target.value)} />
407
+ <button type="button" className="glow-btn" style={{ width: '100%', padding: '9px', fontSize: '0.78rem' }}
408
+ onClick={() => handleAnalyze()} disabled={!issueUrl}>
409
+ Analyze
410
+ </button>
411
+ </div>
412
+ )}
413
+ </>
414
+ )}
415
+ {error && <div style={{ marginTop: 8, fontSize: '0.75rem', color: 'var(--error)' }}>❌ {error}</div>}
416
+ </div>
417
+ </div>
418
+ </div>
419
+ </div>
420
+ );
421
+ }
frontend/src/app/favicon.ico ADDED
frontend/src/app/globals.css ADDED
@@ -0,0 +1,219 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ :root {
2
+ --bg-dark: #0a0a0f;
3
+ --bg-card: rgba(20, 20, 25, 0.7);
4
+ --bg-input: rgba(30, 30, 40, 0.6);
5
+ --border-color: rgba(255, 255, 255, 0.08);
6
+ --accent-glow: rgba(108, 99, 255, 0.3);
7
+ --primary: #8b5cf6;
8
+ --secondary: #6c63ff;
9
+ --text-main: #f0f0ff;
10
+ --text-muted: #9ca3af;
11
+ --success: #10b981;
12
+ --error: #ef4444;
13
+ }
14
+
15
+ * {
16
+ box-sizing: border-box;
17
+ margin: 0;
18
+ padding: 0;
19
+ }
20
+
21
+ html, body {
22
+ height: 100%;
23
+ overflow: hidden;
24
+ }
25
+
26
+ body {
27
+ background-color: var(--bg-dark);
28
+ color: var(--text-main);
29
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
30
+ line-height: 1.5;
31
+ background-image:
32
+ radial-gradient(circle at 15% 50%, rgba(108, 99, 255, 0.05), transparent 25%),
33
+ radial-gradient(circle at 85% 30%, rgba(139, 92, 246, 0.05), transparent 25%);
34
+ }
35
+
36
+ .glass-panel {
37
+ background: var(--bg-card);
38
+ backdrop-filter: blur(12px);
39
+ -webkit-backdrop-filter: blur(12px);
40
+ border: 1px solid var(--border-color);
41
+ border-radius: 16px;
42
+ box-shadow: 0 4px 30px rgba(0, 0, 0, 0.2);
43
+ }
44
+
45
+ .glow-btn {
46
+ background: linear-gradient(135deg, var(--secondary), var(--primary));
47
+ color: white;
48
+ border: none;
49
+ padding: 12px 24px;
50
+ border-radius: 8px;
51
+ font-weight: 600;
52
+ cursor: pointer;
53
+ transition: all 0.3s ease;
54
+ box-shadow: 0 4px 15px var(--accent-glow);
55
+ }
56
+
57
+ .glow-btn:hover {
58
+ transform: translateY(-2px);
59
+ box-shadow: 0 6px 20px rgba(108, 99, 255, 0.5);
60
+ }
61
+
62
+ .glow-btn:disabled {
63
+ opacity: 0.6;
64
+ cursor: not-allowed;
65
+ transform: none;
66
+ box-shadow: none;
67
+ }
68
+
69
+ .input-field {
70
+ width: 100%;
71
+ background: var(--bg-input);
72
+ border: 1px solid var(--border-color);
73
+ color: var(--text-main);
74
+ padding: 14px 16px;
75
+ border-radius: 8px;
76
+ font-size: 1rem;
77
+ transition: border-color 0.2s, box-shadow 0.2s;
78
+ outline: none;
79
+ }
80
+
81
+ .input-field:focus {
82
+ border-color: var(--primary);
83
+ box-shadow: 0 0 0 2px var(--accent-glow);
84
+ }
85
+
86
+ .terminal-window {
87
+ background: #111115;
88
+ border-radius: 12px;
89
+ overflow: hidden;
90
+ border: 1px solid var(--border-color);
91
+ }
92
+
93
+ .terminal-header {
94
+ background: rgba(255, 255, 255, 0.03);
95
+ padding: 10px 16px;
96
+ display: flex;
97
+ align-items: center;
98
+ border-bottom: 1px solid var(--border-color);
99
+ gap: 8px;
100
+ }
101
+
102
+ .terminal-dot {
103
+ width: 12px;
104
+ height: 12px;
105
+ border-radius: 50%;
106
+ }
107
+
108
+ .dot-red { background: #ff5f56; }
109
+ .dot-yellow { background: #ffbd2e; }
110
+ .dot-green { background: #27c93f; }
111
+
112
+ .terminal-body {
113
+ padding: 16px;
114
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
115
+ font-size: 0.9rem;
116
+ color: #a0a0cc;
117
+ max-height: 400px;
118
+ overflow-y: auto;
119
+ }
120
+
121
+ .log-entry {
122
+ margin-bottom: 8px;
123
+ animation: fadeIn 0.3s ease;
124
+ }
125
+
126
+ .log-prefix {
127
+ color: var(--primary);
128
+ margin-right: 8px;
129
+ }
130
+
131
+ .step-card {
132
+ padding: 16px;
133
+ border-radius: 8px;
134
+ background: rgba(255,255,255,0.02);
135
+ border: 1px solid transparent;
136
+ transition: all 0.3s ease;
137
+ }
138
+
139
+ .step-card.active {
140
+ border-color: var(--primary);
141
+ background: rgba(139, 92, 246, 0.05);
142
+ box-shadow: inset 0 0 20px rgba(139, 92, 246, 0.05);
143
+ }
144
+
145
+ .analyzing-glow {
146
+ color: var(--primary) !important;
147
+ background-color: rgba(139, 92, 246, 0.1) !important;
148
+ box-shadow: 0 0 10px rgba(139, 92, 246, 0.2);
149
+ animation: pulseGlow 2s infinite ease-in-out;
150
+ }
151
+
152
+ .terminal-drawer {
153
+ background: #0a0a0f;
154
+ border-top: 1px solid var(--border-color);
155
+ overflow: hidden;
156
+ transition: height 0.3s ease;
157
+ position: relative;
158
+ }
159
+
160
+ .terminal-drawer-header {
161
+ padding: 8px 16px;
162
+ background: rgba(255, 255, 255, 0.02);
163
+ border-bottom: 1px solid var(--border-color);
164
+ font-size: 0.75rem;
165
+ color: var(--text-muted);
166
+ display: flex;
167
+ justify-content: space-between;
168
+ align-items: center;
169
+ cursor: pointer;
170
+ }
171
+
172
+ .terminal-drawer-body {
173
+ padding: 12px 20px;
174
+ max-height: 250px;
175
+ overflow-y: auto;
176
+ font-family: monospace;
177
+ font-size: 0.8rem;
178
+ color: #a0a0cc;
179
+ white-space: pre-wrap;
180
+ }
181
+
182
+ .diff-view {
183
+ background: #1a1a24;
184
+ border-radius: 8px;
185
+ padding: 16px;
186
+ overflow-x: auto;
187
+ font-family: monospace;
188
+ white-space: pre;
189
+ color: #d1d5db;
190
+ }
191
+
192
+ .diff-add { color: #a3be8c; }
193
+ .diff-remove { color: #bf616a; }
194
+ .diff-header { color: #81a1c1; }
195
+
196
+ @keyframes fadeIn {
197
+ from { opacity: 0; transform: translateY(5px); }
198
+ to { opacity: 1; transform: translateY(0); }
199
+ }
200
+
201
+ @keyframes pulseGlow {
202
+ 0% { box-shadow: 0 0 0 0 rgba(139, 92, 246, 0.4); }
203
+ 70% { box-shadow: 0 0 0 10px rgba(139, 92, 246, 0); }
204
+ 100% { box-shadow: 0 0 0 0 rgba(139, 92, 246, 0); }
205
+ }
206
+
207
+ .loading-spinner {
208
+ display: inline-block;
209
+ width: 20px;
210
+ height: 20px;
211
+ border: 2px solid rgba(255,255,255,0.2);
212
+ border-radius: 50%;
213
+ border-top-color: white;
214
+ animation: spin 1s ease-in-out infinite;
215
+ }
216
+
217
+ @keyframes spin {
218
+ to { transform: rotate(360deg); }
219
+ }
frontend/src/app/layout.tsx ADDED
@@ -0,0 +1,30 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import type { Metadata } from "next";
2
+ import { Geist, Geist_Mono } from "next/font/google";
3
+ import "./globals.css";
4
+
5
+ const geistSans = Geist({
6
+ variable: "--font-geist-sans",
7
+ subsets: ["latin"],
8
+ });
9
+
10
+ const geistMono = Geist_Mono({
11
+ variable: "--font-geist-mono",
12
+ subsets: ["latin"],
13
+ });
14
+
15
+ export const metadata: Metadata = {
16
+ title: "FixFlow",
17
+ description: "Autonomous Bug Resolution Agent by Z.ai",
18
+ };
19
+
20
+ export default function RootLayout({
21
+ children,
22
+ }: Readonly<{
23
+ children: React.ReactNode;
24
+ }>) {
25
+ return (
26
+ <html lang="en" className={`${geistSans.variable} ${geistMono.variable}`} suppressHydrationWarning>
27
+ <body suppressHydrationWarning>{children}</body>
28
+ </html>
29
+ );
30
+ }
frontend/src/app/page.tsx ADDED
@@ -0,0 +1,576 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ "use client";
2
+
3
+ import React, { useState, useRef, useEffect } from 'react';
4
+ import { useRouter } from 'next/navigation';
5
+
6
+ type UI_STATE = 'INPUT_REPO' | 'LOADING_REPO' | 'REPO_DASHBOARD' | 'DONE';
7
+
8
+ interface GitHubIssue {
9
+ title: string;
10
+ number: number;
11
+ url: string;
12
+ author: string;
13
+ created_at: string;
14
+ body_snippet: string;
15
+ }
16
+
17
+ interface RepoFile {
18
+ path: string;
19
+ size: number;
20
+ type: string;
21
+ }
22
+
23
+ export default function Home() {
24
+ const router = useRouter();
25
+ const [uiState, setUiState] = useState<UI_STATE>('INPUT_REPO');
26
+ const [repoUrl, setRepoUrl] = useState("");
27
+ const [issueUrl, setIssueUrl] = useState("");
28
+ const [runConfidence, setRunConfidence] = useState(true);
29
+
30
+ const [repoInfo, setRepoInfo] = useState<{ tree: RepoFile[], issues: GitHubIssue[] } | null>(null);
31
+ const [selectedFilePath, setSelectedFilePath] = useState<string | null>(null);
32
+ const [selectedFileContent, setSelectedFileContent] = useState<string>("");
33
+ const [isLoadingFile, setIsLoadingFile] = useState(false);
34
+
35
+ const [statusName, setStatusName] = useState("");
36
+ const [statusMessage, setStatusMessage] = useState("");
37
+ const [running, setRunning] = useState(false);
38
+ const [error, setError] = useState("");
39
+
40
+ const [streamChunks, setStreamChunks] = useState<string[]>([]);
41
+ const [result, setResult] = useState<any>(null);
42
+ const [analyzingFiles, setAnalyzingFiles] = useState<string[]>([]);
43
+ const [terminalExpanded, setTerminalExpanded] = useState(true);
44
+ const [completedSteps, setCompletedSteps] = useState<{step: string, message: string, status: string}[]>([]);
45
+ // When results arrive, show fixed files inline
46
+ const [fixedFileView, setFixedFileView] = useState<{path: string, content: string} | null>(null);
47
+
48
+ const [sessionId, setSessionId] = useState("");
49
+ const [feedback, setFeedback] = useState("");
50
+
51
+ const logsEndRef = useRef<HTMLDivElement>(null);
52
+
53
+ useEffect(() => {
54
+ if (logsEndRef.current) {
55
+ logsEndRef.current.scrollIntoView({ behavior: "smooth" });
56
+ }
57
+ }, [streamChunks, statusMessage, running]);
58
+
59
+ // Handle path detection in status messages to show "live context"
60
+ useEffect(() => {
61
+ if (statusMessage) {
62
+ // Simple path detection (looks for strings with / or .py, .js, etc.)
63
+ const pathRegex = /([a-zA-Z0-9._\-/]+\.(py|js|tsx|ts|html|css|json|md))/g;
64
+ const matches = statusMessage.match(pathRegex);
65
+ if (matches) {
66
+ setAnalyzingFiles(prev => [...new Set([...prev, ...matches])]);
67
+ // Auto-focus the first detected path if it exists in our tree
68
+ const validPath = matches.find(p => repoInfo?.tree.some(f => f.path === p));
69
+ if (validPath && validPath !== selectedFilePath) {
70
+ handleFileClick(validPath);
71
+ }
72
+ }
73
+ }
74
+ }, [statusMessage]);
75
+
76
+ const handleFetchRepo = () => {
77
+ if (!repoUrl) return;
78
+ setError("");
79
+ try {
80
+ // Handle full GitHub URL: https://github.com/owner/repo
81
+ const url = new URL(repoUrl.trim().replace(/\/+$/, ''));
82
+ const parts = url.pathname.replace(/^\//, '').split('/');
83
+ if (parts.length >= 2 && parts[0] && parts[1]) {
84
+ router.push(`/${parts[0]}/${parts[1]}`);
85
+ return;
86
+ }
87
+ } catch {
88
+ // Not a full URL — try treating as "owner/repo"
89
+ const parts = repoUrl.trim().replace(/^github\.com\//, '').split('/');
90
+ if (parts.length >= 2 && parts[0] && parts[1]) {
91
+ router.push(`/${parts[0]}/${parts[1]}`);
92
+ return;
93
+ }
94
+ }
95
+ setError("Please enter a valid GitHub URL (e.g. https://github.com/owner/repo)");
96
+ };
97
+
98
+ const handleFileClick = async (path: string) => {
99
+ setSelectedFilePath(path);
100
+ setIsLoadingFile(true);
101
+ try {
102
+ const res = await fetch(`http://127.0.0.1:8000/api/file_content?repo_url=${encodeURIComponent(repoUrl)}&file_path=${encodeURIComponent(path)}`);
103
+ if (!res.ok) throw new Error("Failed to fetch file content");
104
+ const data = await res.json();
105
+ setSelectedFileContent(data.content);
106
+ } catch (err) {
107
+ setSelectedFileContent("// Error loading file content...");
108
+ } finally {
109
+ setIsLoadingFile(false);
110
+ }
111
+ };
112
+
113
+ const handleAnalyze = (selectedIssueUrl?: string) => {
114
+ const finalIssueUrl = selectedIssueUrl || issueUrl;
115
+ if (!finalIssueUrl || !repoUrl) {
116
+ setError("Please provide an Issue URL");
117
+ return;
118
+ }
119
+
120
+ setRunning(true);
121
+ setError("");
122
+ setResult(null);
123
+ setStreamChunks([]);
124
+ setAnalyzingFiles([]);
125
+ setCompletedSteps([]);
126
+ setFixedFileView(null);
127
+ setTerminalExpanded(true);
128
+ setStatusName("Starting");
129
+ setStatusMessage("Connecting to FixFlow API...");
130
+
131
+ const params = new URLSearchParams({
132
+ issue_url: finalIssueUrl,
133
+ repo_url: repoUrl,
134
+ run_confidence: runConfidence.toString(),
135
+ });
136
+
137
+ const eventSource = new EventSource(`http://127.0.0.1:8000/api/analyze?${params.toString()}`);
138
+ setupEventSource(eventSource);
139
+ };
140
+
141
+ const handleRefine = () => {
142
+ if (!feedback || !sessionId) return;
143
+
144
+ setRunning(true);
145
+ setError("");
146
+ setStreamChunks([]);
147
+ setAnalyzingFiles([]);
148
+ setCompletedSteps([]);
149
+ setFixedFileView(null);
150
+ setTerminalExpanded(true);
151
+ setStatusName("Refining");
152
+ setStatusMessage("Sending feedback to FixFlow...");
153
+
154
+ const params = new URLSearchParams({
155
+ session_id: sessionId,
156
+ feedback: feedback,
157
+ });
158
+
159
+ setFeedback("");
160
+ const eventSource = new EventSource(`http://127.0.0.1:8000/api/refine?${params.toString()}`);
161
+ setupEventSource(eventSource);
162
+ };
163
+
164
+ const setupEventSource = (eventSource: EventSource) => {
165
+ eventSource.addEventListener("status", (e: Event) => {
166
+ const data = JSON.parse((e as MessageEvent).data);
167
+ const { step, status, message } = data;
168
+ setStatusName(step);
169
+ setStatusMessage(message);
170
+ if (status === "complete" || status === "error") {
171
+ setCompletedSteps(prev => [...prev, { step, status, message }]);
172
+ }
173
+ });
174
+
175
+ eventSource.addEventListener("stream", (e: Event) => {
176
+ const data = JSON.parse((e as MessageEvent).data);
177
+ setStreamChunks((prev) => [...prev, data.chunk]);
178
+ });
179
+
180
+ eventSource.addEventListener("done", (e: Event) => {
181
+ const data = JSON.parse((e as MessageEvent).data);
182
+ const r = data.result;
183
+ setResult(r);
184
+ if (data.session_id) setSessionId(data.session_id);
185
+
186
+ // Auto-load the first fixed file into the editor so the user sees the change
187
+ if (r?.fixed_files) {
188
+ const paths = Object.keys(r.fixed_files);
189
+ if (paths.length > 0) {
190
+ const firstPath = paths[0];
191
+ setFixedFileView({ path: firstPath, content: r.fixed_files[firstPath] });
192
+ setSelectedFilePath(firstPath);
193
+ }
194
+ }
195
+ });
196
+
197
+ eventSource.addEventListener("error", (e: Event) => {
198
+ const data = JSON.parse((e as MessageEvent).data);
199
+ setError(data.error || "Unknown stream error");
200
+ setRunning(false);
201
+ eventSource.close();
202
+ });
203
+
204
+ eventSource.addEventListener("eof", () => {
205
+ setRunning(false);
206
+ setStatusName("Done");
207
+ setStatusMessage("Fix generated — Review the highlighted file in the editor.");
208
+ setCompletedSteps(prev => [...prev, { step: "done", status: "complete", message: "Fix generated — review the highlighted file in the editor!" }]);
209
+ eventSource.close();
210
+ // Stay on REPO_DASHBOARD — the fix is shown in the editor
211
+ });
212
+
213
+ eventSource.addEventListener("heartbeat", () => {});
214
+
215
+ eventSource.onerror = (e) => {
216
+ console.error("SSE Error:", e);
217
+ };
218
+ };
219
+
220
+ const handleOpenPR = async () => {
221
+ if (!result) return;
222
+ try {
223
+ const res = await fetch("http://127.0.0.1:8000/api/pr", {
224
+ method: "POST",
225
+ headers: { "Content-Type": "application/json" },
226
+ body: JSON.stringify({
227
+ repo_url: repoUrl,
228
+ title: result.issue_title,
229
+ body: result.fix_explanation + "\n\n---\n*Generated autonomously by FixFlow*",
230
+ fixed_files: result.fixed_files
231
+ })
232
+ });
233
+
234
+ const data = await res.json();
235
+ if (!res.ok) throw new Error(data.detail || "Failed to create PR");
236
+ window.open(data.url, "_blank");
237
+ } catch (err: any) {
238
+ alert("Error: " + err.message);
239
+ }
240
+ };
241
+
242
+ // ── Render Helpers ────────────────────────────────────────────────────────
243
+
244
+ const renderInputRepo = () => (
245
+ <div className="glass-panel" style={{ padding: 40, marginBottom: 40, animation: 'fadeIn 0.5s ease' }}>
246
+ <h2 style={{ marginBottom: 24, fontSize: '1.5rem', fontWeight: 600 }}>🚀 Connect a Repository</h2>
247
+ <p style={{ color: 'var(--text-muted)', marginBottom: 24 }}>Enter a GitHub URL to start the autonomous bug resolution process.</p>
248
+
249
+ <div style={{ marginBottom: 32 }}>
250
+ <label style={{ display: 'block', marginBottom: 8, fontSize: '0.9rem', color: 'var(--text-muted)' }}>Repository URL</label>
251
+ <input
252
+ className="input-field"
253
+ placeholder="https://github.com/owner/repo"
254
+ value={repoUrl}
255
+ onChange={(e) => setRepoUrl(e.target.value)}
256
+ onKeyDown={(e) => e.key === 'Enter' && handleFetchRepo()}
257
+ />
258
+ </div>
259
+
260
+ <button
261
+ type="button"
262
+ className="glow-btn"
263
+ style={{ width: '100%', fontSize: '1.1rem', padding: '16px' }}
264
+ onClick={handleFetchRepo}
265
+ disabled={!repoUrl || running}
266
+ >
267
+ {running ? "Fetching..." : "Clone & Explore Repo"}
268
+ </button>
269
+
270
+ {error && <div style={{ marginTop: 20, color: 'var(--error)', backgroundColor: 'rgba(239, 68, 68, 0.1)', padding: 12, borderRadius: 8 }}>❌ {error}</div>}
271
+ </div>
272
+ );
273
+
274
+ const renderLoadingRepo = () => (
275
+ <div className="glass-panel" style={{ padding: 60, textAlign: 'center', marginBottom: 40 }}>
276
+ <div className="loading-spinner" style={{ width: 40, height: 40, marginBottom: 24 }} />
277
+ <h2 style={{ fontSize: '1.5rem', fontWeight: 600, marginBottom: 8 }}>Indexing Repository...</h2>
278
+ <p style={{ color: 'var(--text-muted)' }}>We're fetching the file tree and discovering open issues from GitHub.</p>
279
+ </div>
280
+ );
281
+
282
+ const renderRepoDashboard = () => {
283
+ if (!repoInfo) return null;
284
+ return (
285
+ <div style={{ animation: 'fadeIn 0.5s ease', height: '100%', display: 'flex', flexDirection: 'column' }}>
286
+ <div style={{ display: 'grid', gridTemplateColumns: '240px 1fr 300px', gap: 16, flex: 1, minHeight: 0 }}>
287
+
288
+ {/* Left Pane: Explorer with analyzing-glow */}
289
+ <div className="glass-panel" style={{ padding: 16, display: 'flex', flexDirection: 'column' }}>
290
+ <h3 style={{ marginBottom: 16, fontSize: '0.9rem', color: 'var(--text-main)', opacity: 0.8 }}>📁 EXPLORER</h3>
291
+ <div style={{ flex: 1, overflowY: 'auto', fontSize: '0.8rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
292
+ {repoInfo.tree.map((file, i) => {
293
+ const isAnalyzing = analyzingFiles.includes(file.path);
294
+ return (
295
+ <div
296
+ key={i}
297
+ onClick={() => handleFileClick(file.path)}
298
+ className={isAnalyzing ? 'analyzing-glow' : ''}
299
+ style={{
300
+ padding: '6px 8px',
301
+ cursor: 'pointer',
302
+ borderRadius: '4px',
303
+ marginBottom: '2px',
304
+ backgroundColor: selectedFilePath === file.path ? 'rgba(139, 92, 246, 0.15)' : 'transparent',
305
+ color: selectedFilePath === file.path ? 'var(--primary)' : 'inherit',
306
+ whiteSpace: 'nowrap',
307
+ overflow: 'hidden',
308
+ textOverflow: 'ellipsis',
309
+ transition: 'all 0.3s ease'
310
+ }}
311
+ >
312
+ <span style={{ marginRight: 6 }}>{isAnalyzing ? '🧠' : '📄'}</span>
313
+ {file.path}
314
+ </div>
315
+ );
316
+ })}
317
+ </div>
318
+ </div>
319
+
320
+ {/* Center Pane: Editor + Integrated Step Tracker */}
321
+ <div style={{ display: 'flex', flexDirection: 'column', height: '100%', gap: 0 }}>
322
+ {/* Editor */}
323
+ <div className="terminal-window" style={{ flex: 1, display: 'flex', flexDirection: 'column', position: 'relative', borderBottomLeftRadius: 0, borderBottomRightRadius: 0 }}>
324
+ <div className="terminal-header">
325
+ <div className="terminal-dot dot-red"></div>
326
+ <div className="terminal-dot dot-yellow"></div>
327
+ <div className="terminal-dot dot-green"></div>
328
+ <div style={{ marginLeft: '12px', fontSize: '0.75rem', color: 'var(--text-muted)', fontFamily: 'monospace' }}>
329
+ {/* Show streaming code in editor during step 4 */}
330
+ {running && (statusName === '4_fix' || statusName === '4_refine')
331
+ ? '🔧 Generating Fix...'
332
+ : selectedFilePath || 'Editor'}
333
+ </div>
334
+ </div>
335
+ <div style={{ flex: 1, overflow: 'auto', backgroundColor: '#0d0d12' }}>
336
+ {/* Priority 1: Fix just generated — show it in the editor with green */}
337
+ {fixedFileView && !running ? (
338
+ <pre style={{ margin: 0, padding: '24px', fontSize: '0.85rem', fontFamily: 'monospace', color: '#a3be8c', lineHeight: '1.6' }}>
339
+ {fixedFileView.content}
340
+ </pre>
341
+ ) : running && (statusName === '4_fix' || statusName === '4_refine') && streamChunks.length > 0 ? (
342
+ /* Priority 2: Stream the fix being generated live */
343
+ <pre style={{ margin: 0, padding: '24px', fontSize: '0.85rem', fontFamily: 'monospace', color: '#a3be8c', lineHeight: '1.6' }}>
344
+ {streamChunks.join('')}
345
+ <span style={{ borderRight: '2px solid var(--primary)' }}> </span>
346
+ <div ref={logsEndRef} />
347
+ </pre>
348
+ ) : (
349
+ /* Default: show selected file */
350
+ <pre style={{ margin: 0, padding: '24px', fontSize: '0.85rem', fontFamily: 'monospace', color: '#d1d5db', lineHeight: '1.6' }}>
351
+ {selectedFileContent || '// Select a file to browse source code'}
352
+ </pre>
353
+ )}
354
+ </div>
355
+ </div>
356
+
357
+ {/* Bottom Step Progress Tracker */}
358
+ <div className="terminal-drawer" style={{ height: terminalExpanded ? '220px' : '40px', borderRadius: '0 0 12px 12px' }}>
359
+ <div className="terminal-drawer-header" onClick={() => setTerminalExpanded(!terminalExpanded)}>
360
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10 }}>
361
+ {running && <span className="loading-spinner" style={{ width: 12, height: 12 }} />}
362
+ <span style={{ color: running ? 'var(--primary)' : 'var(--text-muted)' }}>
363
+ {statusName ? `AGENT — [${statusName.toUpperCase()}]` : 'FIXFLOW AGENT CONSOLE'}
364
+ </span>
365
+ </div>
366
+ <span>{terminalExpanded ? '▼' : '▲ Show Progress'}</span>
367
+ </div>
368
+ <div className="terminal-drawer-body" style={{ padding: '12px 16px' }}>
369
+ {/* Completed steps */}
370
+ {completedSteps.map((s, i) => (
371
+ <div key={i} style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 8, fontSize: '0.8rem' }}>
372
+ <span style={{ color: s.status === 'error' ? 'var(--error)' : 'var(--success)', flexShrink: 0 }}>
373
+ {s.status === 'error' ? '✗' : '✓'}
374
+ </span>
375
+ <span style={{ color: 'var(--text-muted)' }}>{s.message}</span>
376
+ </div>
377
+ ))}
378
+ {/* Current running step */}
379
+ {running && statusMessage && (
380
+ <div style={{ display: 'flex', alignItems: 'center', gap: 10, fontSize: '0.8rem' }}>
381
+ <span className="loading-spinner" style={{ width: 10, height: 10, flexShrink: 0 }} />
382
+ <span style={{ color: 'var(--primary)' }}>{statusMessage}</span>
383
+ </div>
384
+ )}
385
+ <div ref={logsEndRef} />
386
+ </div>
387
+ </div>
388
+ </div>
389
+
390
+
391
+ {/* Right Pane: Discovery OR Result Actions */}
392
+ <div className="glass-panel" style={{ padding: 16, display: 'flex', flexDirection: 'column' }}>
393
+ {result && !running ? (
394
+ /* ── Result Actions Panel ── */
395
+ <>
396
+ <h3 style={{ marginBottom: 4, fontSize: '0.9rem', color: 'var(--success)', display: 'flex', alignItems: 'center', gap: 8 }}>
397
+ <span style={{ fontSize: '1.1rem' }}>✅</span> FIX READY
398
+ </h3>
399
+ <p style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: 16 }}>
400
+ {result.bug_summary?.slice(0, 120)}...
401
+ </p>
402
+
403
+ {/* Fixed files list — click to view each */}
404
+ <div style={{ marginBottom: 16 }}>
405
+ <div style={{ fontSize: '0.75rem', color: 'var(--text-muted)', marginBottom: 8 }}>CHANGED FILES:</div>
406
+ {Object.keys(result.fixed_files || {}).map((path) => (
407
+ <div
408
+ key={path}
409
+ onClick={() => setFixedFileView({ path, content: result.fixed_files[path] })}
410
+ style={{
411
+ padding: '6px 10px',
412
+ borderRadius: 6,
413
+ marginBottom: 6,
414
+ cursor: 'pointer',
415
+ fontSize: '0.75rem',
416
+ fontFamily: 'monospace',
417
+ backgroundColor: fixedFileView?.path === path ? 'rgba(163, 190, 140, 0.15)' : 'rgba(255,255,255,0.04)',
418
+ color: fixedFileView?.path === path ? '#a3be8c' : 'var(--text-muted)',
419
+ border: fixedFileView?.path === path ? '1px solid rgba(163,190,140,0.3)' : '1px solid transparent',
420
+ whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis'
421
+ }}
422
+ >
423
+ 📝 {path}
424
+ </div>
425
+ ))}
426
+ </div>
427
+
428
+ <div style={{ flex: 1 }} />
429
+
430
+ {/* Action Buttons */}
431
+ <button type="button" className="glow-btn" style={{ width: '100%', marginBottom: 10, padding: '10px', fontSize: '0.85rem' }} onClick={handleOpenPR}>
432
+ 🚀 Open Pull Request
433
+ </button>
434
+
435
+ <div style={{ borderTop: '1px solid var(--border-color)', paddingTop: 12, marginTop: 4, display: 'flex', gap: 8 }}>
436
+ <input
437
+ className="input-field"
438
+ style={{ flex: 1, padding: '8px 10px', fontSize: '0.75rem' }}
439
+ placeholder="Refine the fix..."
440
+ value={feedback}
441
+ onChange={(e) => setFeedback(e.target.value)}
442
+ onKeyDown={(e) => e.key === 'Enter' && handleRefine()}
443
+ />
444
+ <button type="button" className="glow-btn" style={{ padding: '8px 12px', fontSize: '0.75rem' }} onClick={handleRefine} disabled={!feedback}>
445
+
446
+ </button>
447
+ </div>
448
+
449
+ <button
450
+ type="button"
451
+ style={{ marginTop: 10, background: 'none', border: 'none', color: 'var(--text-muted)', cursor: 'pointer', fontSize: '0.75rem', width: '100%' }}
452
+ onClick={() => { setResult(null); setFixedFileView(null); setCompletedSteps([]); }}
453
+ >
454
+ ← Start a new issue
455
+ </button>
456
+ </>
457
+ ) : (
458
+ /* ── Discovery Panel ── */
459
+ <>
460
+ <h3 style={{ marginBottom: 16, fontSize: '0.9rem', color: 'var(--text-main)', opacity: 0.8, display: 'flex', justifyContent: 'space-between' }}>
461
+ <span>🐛 DISCOVERY</span>
462
+ {!running && (
463
+ <button onClick={() => setUiState('INPUT_REPO')} style={{ background: 'none', border: 'none', color: 'var(--primary)', cursor: 'pointer', fontSize: '0.7rem' }}>Change Repo</button>
464
+ )}
465
+ </h3>
466
+ <div style={{ flex: 1, overflowY: 'auto' }}>
467
+ {repoInfo.issues.map((issue) => (
468
+ <div
469
+ key={issue.number}
470
+ className="step-card"
471
+ style={{ cursor: running ? 'default' : 'pointer', border: '1px solid rgba(255,255,255,0.05)', padding: '12px', marginBottom: 10, opacity: running ? 0.5 : 1 }}
472
+ onClick={() => !running && handleAnalyze(issue.url)}
473
+ >
474
+ <div style={{ fontWeight: 600, color: 'var(--text-main)', marginBottom: 4, fontSize: '0.8rem' }}>#{issue.number} {issue.title}</div>
475
+ <div style={{ fontSize: '0.7rem', color: 'var(--text-muted)' }}>@{issue.author}</div>
476
+ </div>
477
+ ))}
478
+ </div>
479
+ {!running && (
480
+ <div style={{ borderTop: '1px solid var(--border-color)', paddingTop: 16, marginTop: 16 }}>
481
+ <input className="input-field" style={{ padding: '8px 12px', fontSize: '0.8rem', marginBottom: 8 }} placeholder="Paste direct Issue URL..." value={issueUrl} onChange={(e) => setIssueUrl(e.target.value)} />
482
+ <button type="button" className="glow-btn" style={{ width: '100%', padding: '10px', fontSize: '0.8rem' }} onClick={() => handleAnalyze()} disabled={!issueUrl}>Analyze Manually</button>
483
+ </div>
484
+ )}
485
+ </>
486
+ )}
487
+ </div>
488
+ </div>
489
+ </div>
490
+ );
491
+ };
492
+
493
+ const renderResult = () => (
494
+ <div style={{ animation: 'fadeIn 0.5s ease', maxWidth: '1000px', margin: '0 auto' }}>
495
+ <div className="glass-panel" style={{ padding: 32, marginBottom: 24 }}>
496
+ <h2 style={{ marginBottom: 16, color: 'var(--success)' }}>✅ Fix Successfully Generated</h2>
497
+ <p style={{ marginBottom: 24, padding: 16, background: 'rgba(0,0,0,0.2)', borderRadius: 8, fontSize: '0.95rem' }}>
498
+ {result.bug_summary}
499
+ </p>
500
+
501
+ <h3 style={{ marginBottom: 16, borderBottom: '1px solid var(--border-color)', paddingBottom: 8 }}>Analysis & Methodology</h3>
502
+ <p style={{ marginBottom: 24, whiteSpace: 'pre-wrap', fontSize: '0.95rem', color: '#d1d5db' }}>
503
+ {result.root_cause_analysis}
504
+ </p>
505
+
506
+ <h3 style={{ marginBottom: 16, borderBottom: '1px solid var(--border-color)', paddingBottom: 8 }}>Unified Diff</h3>
507
+ <div className="diff-view" style={{ marginBottom: 24 }}>
508
+ {result.diff_formatted.split('\n').map((line: string, i: number) => {
509
+ let className = "";
510
+ if (line.startsWith('+') && !line.startsWith('+++')) className = "diff-add";
511
+ else if (line.startsWith('-') && !line.startsWith('---')) className = "diff-remove";
512
+ else if (line.startsWith('@@') || line.startsWith('---') || line.startsWith('+++')) className = "diff-header";
513
+ return <div key={i} className={className}>{line}</div>
514
+ })}
515
+ </div>
516
+
517
+ <div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
518
+ <button
519
+ type="button"
520
+ className="glow-btn"
521
+ style={{ background: 'rgba(255,255,255,0.05)', boxShadow: 'none', border: '1px solid var(--border-color)' }}
522
+ onClick={() => {
523
+ setResult(null);
524
+ setSessionId("");
525
+ setStreamChunks([]);
526
+ setUiState('INPUT_REPO');
527
+ }}
528
+ >
529
+ Start New Task
530
+ </button>
531
+ <button type="button" className="glow-btn" onClick={handleOpenPR} disabled={running}>
532
+ 🚀 Open GitHub Pull Request
533
+ </button>
534
+ </div>
535
+ </div>
536
+
537
+ <div className="glass-panel" style={{ padding: 24, marginTop: 24, display: 'flex', gap: 12 }}>
538
+ <input
539
+ className="input-field"
540
+ placeholder="Refine the fix? Tell the agent what to change..."
541
+ value={feedback}
542
+ onChange={(e) => setFeedback(e.target.value)}
543
+ onKeyDown={(e) => e.key === 'Enter' && handleRefine()}
544
+ style={{ flex: 1 }}
545
+ />
546
+ <button
547
+ type="button"
548
+ className="glow-btn"
549
+ onClick={handleRefine}
550
+ disabled={!feedback || running}
551
+ >
552
+ {running ? 'Refining...' : 'Refine'}
553
+ </button>
554
+ </div>
555
+ </div>
556
+ );
557
+
558
+ return (
559
+ <div style={{ height: '100vh', display: 'flex', flexDirection: 'column', overflow: 'hidden' }}>
560
+ {/* Compact top bar */}
561
+ <div style={{ display: 'flex', alignItems: 'center', gap: 12, padding: '10px 24px', borderBottom: '1px solid var(--border-color)', flexShrink: 0 }}>
562
+ <span style={{ fontSize: '1.4rem' }}>🔧</span>
563
+ <span style={{ fontWeight: 800, fontSize: '1.2rem', background: 'linear-gradient(135deg, var(--primary), var(--secondary))', WebkitBackgroundClip: 'text', WebkitTextFillColor: 'transparent' }}>FixFlow</span>
564
+ <span style={{ color: 'var(--text-muted)', fontSize: '0.7rem', letterSpacing: '0.12em', marginLeft: 4 }}>AUTONOMOUS REPOSITORY AGENT</span>
565
+ </div>
566
+
567
+ {/* Main content area */}
568
+ <div style={{ flex: 1, overflow: 'auto', padding: uiState === 'REPO_DASHBOARD' ? '16px' : '40px 20px', display: 'flex', flexDirection: 'column' }}>
569
+ {uiState === 'INPUT_REPO' && renderInputRepo()}
570
+ {uiState === 'LOADING_REPO' && renderLoadingRepo()}
571
+ {uiState === 'REPO_DASHBOARD' && renderRepoDashboard()}
572
+ {uiState === 'DONE' && renderResult()}
573
+ </div>
574
+ </div>
575
+ );
576
+ }
frontend/tsconfig.json ADDED
@@ -0,0 +1,34 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2017",
4
+ "lib": ["dom", "dom.iterable", "esnext"],
5
+ "allowJs": true,
6
+ "skipLibCheck": true,
7
+ "strict": true,
8
+ "noEmit": true,
9
+ "esModuleInterop": true,
10
+ "module": "esnext",
11
+ "moduleResolution": "bundler",
12
+ "resolveJsonModule": true,
13
+ "isolatedModules": true,
14
+ "jsx": "react-jsx",
15
+ "incremental": true,
16
+ "plugins": [
17
+ {
18
+ "name": "next"
19
+ }
20
+ ],
21
+ "paths": {
22
+ "@/*": ["./src/*"]
23
+ }
24
+ },
25
+ "include": [
26
+ "next-env.d.ts",
27
+ "**/*.ts",
28
+ "**/*.tsx",
29
+ ".next/types/**/*.ts",
30
+ ".next/dev/types/**/*.ts",
31
+ "**/*.mts"
32
+ ],
33
+ "exclude": ["node_modules"]
34
+ }
requirements.txt CHANGED
@@ -3,3 +3,6 @@ openai>=1.0.0
3
  PyGithub>=2.1.0
4
  requests>=2.31.0
5
  python-dotenv>=1.0.0
 
 
 
 
3
  PyGithub>=2.1.0
4
  requests>=2.31.0
5
  python-dotenv>=1.0.0
6
+ fastapi
7
+ uvicorn
8
+ sse-starlette