Spaces:
Running
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 +78 -5
- backend/api.py +275 -0
- backend/github_client.py +35 -0
- backend/llm_client.py +71 -24
- frontend/.gitignore +41 -0
- frontend/AGENTS.md +5 -0
- frontend/CLAUDE.md +1 -0
- frontend/README.md +36 -0
- frontend/eslint.config.mjs +18 -0
- frontend/next.config.ts +7 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +24 -0
- frontend/public/file.svg +1 -0
- frontend/public/globe.svg +1 -0
- frontend/public/next.svg +1 -0
- frontend/public/vercel.svg +1 -0
- frontend/public/window.svg +1 -0
- frontend/src/app/[owner]/[repo]/page.tsx +421 -0
- frontend/src/app/favicon.ico +0 -0
- frontend/src/app/globals.css +219 -0
- frontend/src/app/layout.tsx +30 -0
- frontend/src/app/page.tsx +576 -0
- frontend/tsconfig.json +34 -0
- requirements.txt +3 -0
|
@@ -138,12 +138,13 @@ class FixFlowAgent:
|
|
| 138 |
t1 = time.time()
|
| 139 |
|
| 140 |
result.bug_summary = self._step1_issue_understanding(
|
| 141 |
-
result.issue_data,
|
| 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 |
-
|
| 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 |
-
|
| 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 = [
|
|
@@ -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())
|
|
@@ -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(
|
|
@@ -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
|
| 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 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
|
|
|
|
|
|
|
|
|
| 63 |
|
| 64 |
-
|
| 65 |
-
|
|
|
|
|
|
|
| 66 |
|
| 67 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
+
|
|
@@ -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
|
|
@@ -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 -->
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
@AGENTS.md
|
|
@@ -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.
|
|
@@ -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;
|
|
@@ -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;
|
|
The diff for this file is too large to render.
See raw diff
|
|
|
|
@@ -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 |
+
}
|
|
|
|
|
|
|
|
|
|
|
|
@@ -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 |
+
}
|
|
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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 |
+
}
|
|
@@ -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
|