From a7eaac53f8b04d961ae1b8fdf715a87c1d25b779 Mon Sep 17 00:00:00 2001 From: Void-Anvesha Date: Thu, 11 Jun 2026 14:37:59 +0530 Subject: [PATCH 1/6] hightlight-detection --- README.md | 9 +- frontend/assets/icon.svg | 14 +++ frontend/index.html | 225 +++++++++++++++++++++++++++++++++++++-- 3 files changed, 241 insertions(+), 7 deletions(-) create mode 100644 frontend/assets/icon.svg diff --git a/README.md b/README.md index d38e8e5b..f3d435ae 100644 --- a/README.md +++ b/README.md @@ -102,8 +102,15 @@ cd AI-dev-assistant ```bash cd backend pip install -r requirements.txt -uvicorn app.main:app --reload +python -m uvicorn app.main:app --reload ``` + +If `uvicorn` is not recognized on Windows, make sure you installed dependencies in the same Python environment and run: + +```powershell +python -m uvicorn app.main:app --reload +``` + ### Environment Setup Copy `.env.example` to `.env` diff --git a/frontend/assets/icon.svg b/frontend/assets/icon.svg new file mode 100644 index 00000000..21636a4d --- /dev/null +++ b/frontend/assets/icon.svg @@ -0,0 +1,14 @@ + + QyverixAI icon + QyverixAI hexagon icon mark with search lens + + + + + + + + + + + diff --git a/frontend/index.html b/frontend/index.html index 11ff77ac..1377f1fc 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -659,6 +659,128 @@ text-align: right; background: var(--bg3); border-right: 1px solid var(--border); + overflow: hidden; + z-index: 2; + } + + .line-number-row { + display: block; + height: 22.1px; + padding-right: 10px; + position: relative; + } + + .line-number-row.has-issue { + color: var(--text); + font-weight: 600; + } + + .line-number-row.has-issue::before { + content: ''; + position: absolute; + left: 4px; + top: 50%; + width: 6px; + height: 6px; + border-radius: 50%; + transform: translateY(-50%); + background: var(--accent); + box-shadow: 0 0 0 3px rgba(91, 156, 246, 0.14); + } + + .line-number-row.issue-error::before { + background: var(--red); + box-shadow: 0 0 0 3px rgba(242, 87, 87, 0.14); + } + + .line-number-row.issue-warning::before { + background: var(--yellow); + box-shadow: 0 0 0 3px rgba(245, 200, 66, 0.14); + } + + .line-number-row.issue-info::before { + background: var(--accent); + box-shadow: 0 0 0 3px rgba(91, 156, 246, 0.14); + } + + .code-editor-shell { + flex: 1; + min-width: 0; + position: relative; + } + + .issue-line-highlights { + position: absolute; + inset: 0; + padding: 16px 0; + pointer-events: none; + overflow: hidden; + z-index: 0; + } + + .issue-highlight-track { + transform: translateY(0); + will-change: transform; + } + + .issue-highlight-row { + height: 22.1px; + border-left: 3px solid transparent; + opacity: 0.95; + position: relative; + } + + .issue-highlight-row::after { + content: attr(data-label); + position: absolute; + top: 3px; + right: 12px; + padding: 1px 7px; + border-radius: 999px; + font-family: var(--font-ui); + font-size: 0.62rem; + font-weight: 700; + letter-spacing: 0.04em; + line-height: 1.4; + text-transform: uppercase; + opacity: 0; + transform: translateX(4px); + transition: opacity var(--transition), transform var(--transition); + } + + .editor-wrap:hover .issue-highlight-row::after { + opacity: 1; + transform: translateX(0); + } + + .issue-highlight-row.issue-error { + border-left-color: var(--red); + background: linear-gradient(90deg, rgba(242, 87, 87, 0.24), rgba(242, 87, 87, 0.07) 70%, transparent); + } + + .issue-highlight-row.issue-error::after { + color: var(--red); + background: rgba(242, 87, 87, 0.14); + } + + .issue-highlight-row.issue-warning { + border-left-color: var(--yellow); + background: linear-gradient(90deg, rgba(245, 200, 66, 0.24), rgba(245, 200, 66, 0.07) 70%, transparent); + } + + .issue-highlight-row.issue-warning::after { + color: var(--yellow); + background: rgba(245, 200, 66, 0.14); + } + + .issue-highlight-row.issue-info { + border-left-color: var(--accent); + background: linear-gradient(90deg, rgba(91, 156, 246, 0.22), rgba(91, 156, 246, 0.06) 70%, transparent); + } + + .issue-highlight-row.issue-info::after { + color: var(--accent); + background: rgba(91, 156, 246, 0.14); } #codeEditor { @@ -675,6 +797,8 @@ outline: none; resize: vertical; tab-size: 2; + position: relative; + z-index: 1; } .editor-footer { @@ -2187,7 +2311,10 @@

File Upload

1
- +
+ + +
`; text += issues.map(i => `${i.type}: ${i.description}\nFix: ${i.suggestion}`).join('\n') + '\n\n'; + + // Apply line highlights if issues exist + if (issues.length > 0) { + applyLineHighlights(issues); + } } if (data.suggestions) { const sg = data.suggestions; @@ -497,6 +609,9 @@ function renderResult(data, mode) { `; text = issues.map(i => `[${i.type}] Line ${i.line}: ${i.description}\nFix: ${i.suggestion}`).join('\n'); + + // Apply line highlights for debugging mode + applyLineHighlights(issues); } else if (mode === 'suggestions') { const cards = data.suggestions || []; html += `
diff --git a/frontend/style.css b/frontend/style.css index b05b82a7..c4d1467a 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -361,8 +361,10 @@ body { } .code-editor { + position: relative; + z-index: 1; flex: 1; - background: var(--bg); + background: transparent; border: none; outline: none; color: var(--text); @@ -372,9 +374,146 @@ body { padding: 16px; resize: none; min-height: 380px; + caret-color: var(--text); } .code-editor::placeholder { color: var(--text-3); } +/* ── Line highlighting system ── */ +.editor-wrap { + position: relative; + flex: 1; + display: flex; + overflow: hidden; +} + +.line-numbers { + display: flex; + flex-direction: column; + padding: 16px 8px; + background: var(--bg-2); + border-right: 1px solid var(--border); + color: var(--text-3); + font-family: var(--font-mono); + font-size: 13px; + line-height: 1.7; + text-align: right; + user-select: none; + min-width: 40px; + pointer-events: none; +} + +.code-editor-shell { + position: relative; + flex: 1; + display: flex; + flex-direction: column; + background: var(--bg); +} + +.issue-line-highlights { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 0; + pointer-events: none; +} + +.issue-line-highlight { + position: absolute; + left: 0; + right: 0; + height: calc(1.7 * 13px); + opacity: 0; + transition: opacity var(--transition); + pointer-events: auto; +} + +.issue-line-highlight:hover { + opacity: 1 !important; +} + +.issue-line-highlight.error { + background: rgba(239, 68, 68, 0.15); + border-left: 3px solid #ef4444; +} + +.issue-line-highlight.warning { + background: rgba(234, 179, 8, 0.15); + border-left: 3px solid #eab308; +} + +.issue-line-highlight.info { + background: rgba(59, 130, 246, 0.15); + border-left: 3px solid #3b82f6; +} + +.issue-line-highlight.active { + opacity: 0.6; +} + +.issue-tooltip { + position: absolute; + background: var(--bg-3); + color: var(--text); + border: 1px solid var(--border); + border-radius: 4px; + padding: 8px 10px; + font-size: 11px; + font-family: var(--font-mono); + line-height: 1.4; + white-space: nowrap; + pointer-events: none; + z-index: 1000; + box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4); +} + +.issue-tooltip.error { + border-left: 3px solid #ef4444; +} + +.issue-tooltip.warning { + border-left: 3px solid #eab308; +} + +.issue-tooltip.info { + border-left: 3px solid #3b82f6; +} + +.issue-gutter-marker { + position: absolute; + left: 4px; + width: 20px; + height: 20px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 50%; + font-size: 10px; + font-weight: bold; + cursor: pointer; + transition: all var(--transition); +} + +.issue-gutter-marker.error { + background: rgba(239, 68, 68, 0.2); + color: #ef4444; +} + +.issue-gutter-marker.warning { + background: rgba(234, 179, 8, 0.2); + color: #eab308; +} + +.issue-gutter-marker.info { + background: rgba(59, 130, 246, 0.2); + color: #3b82f6; +} + +.issue-gutter-marker:hover { + transform: scale(1.2); +} + .editor-footer { display: flex; justify-content: space-between; From 2e6bf8ef336e6d6cf58836399f5d6dac8f6c7675 Mon Sep 17 00:00:00 2001 From: Void-Anvesha Date: Thu, 23 Jul 2026 22:47:51 +0530 Subject: [PATCH 4/6] Added chatbot for follow-up Q&A --- CHATBOT_FEATURE.md | 0 CONTRIBUTING.md | 63 ++++++++++++++ backend/app/routers/chat.py | 16 +++- backend/app/schemas.py | 52 ++++++++++-- backend/tests/test_chat.py | 22 +++++ frontend/index.html | 165 ++++++++++++++++++++++++++++++++++++ frontend/script.js | 117 ++++++++++++++++++++++++- frontend/style.css | 34 ++++++++ 8 files changed, 455 insertions(+), 14 deletions(-) create mode 100644 CHATBOT_FEATURE.md create mode 100644 backend/tests/test_chat.py diff --git a/CHATBOT_FEATURE.md b/CHATBOT_FEATURE.md new file mode 100644 index 00000000..e69de29b diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 803085f3..ca93a166 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -88,6 +88,69 @@ The entire frontend is `frontend/index.html` - one self-contained file. No build --- +## Optional LLM / API Key setup (safe for open-source) + +QyverixAI can run fully offline using the built-in rule-based engine. If you opt-in to richer LLM-powered replies, follow these steps to provide an API key safely. + +- Use the provided example file: copy `.env.example` to `.env` and edit values locally. The repo already includes `.env.example` and `.gitignore` ignores `.env`. + + ```bash + # from repo root (Unix/macOS) + cp .env.example .env + # or on Windows PowerShell + Copy-Item .env.example .env + ``` + +- Edit `backend/.env` (or `backend/.env.local`) and set these values: + + ```text + LLM_ENABLED=true + LLM_API_KEY=sk_your_openai_key_here + LLM_BASE_URL=https://api.openai.com/v1 + LLM_MODEL=gpt-4o-mini + ``` + +- Important: do NOT commit `.env`. The repository `.gitignore` already excludes `.env`. To be safe, check with: + + ```bash + git status --ignored -- .env + ``` + +- Alternative: set env vars only for your shell session (no file written): + + PowerShell (temporary for session): + ```powershell + $env:LLM_ENABLED = "true" + $env:LLM_API_KEY = "sk_..." + cd backend + python -m uvicorn app.main:app --reload + ``` + + Unix / macOS (temporary for session): + ```bash + export LLM_ENABLED=true + export LLM_API_KEY=sk_... + cd backend + python -m uvicorn app.main:app --reload + ``` + +- CI / Deployment: configure the provider's secrets or environment variables (GitHub Actions Secrets, Render dashboard, Docker secrets, etc.) rather than storing keys in the repo. Example for GitHub Actions `workflow.yml`: + + ```yaml + env: + LLM_ENABLED: true + secrets: + LLM_API_KEY: ${{ secrets.LLM_API_KEY }} + ``` + +- Local LLM option: If you prefer no external keys, you can run an on-host LLM (Ollama, local Llama) and set `LLM_BASE_URL` to the local endpoint. This keeps everything on your machine. + +- If you want us to improve the built-in fallback (rule-based) to provide more detailed, actionable answers without an API key, we can do that — it's the default behavior. + +If you want, I can add a short `LLM_SETUP.md` with screenshots and copy-ready snippets for Render/GitHub Actions — tell me which host you'd like docs for. + +--- + ## Pull Request Checklist Before opening a PR, confirm: diff --git a/backend/app/routers/chat.py b/backend/app/routers/chat.py index 8093a374..5db45029 100644 --- a/backend/app/routers/chat.py +++ b/backend/app/routers/chat.py @@ -10,10 +10,14 @@ @router.post("", response_model=ChatResponse) async def chat(payload: ChatRequest) -> ChatResponse: + prompt = payload.message or "" + if payload.context: + prompt = f"{payload.context}\n\n{prompt}" if prompt else payload.context + if llm_analysis_client.enabled: try: reply = await llm_analysis_client.chat_reply( - message=payload.message, + message=prompt, code=payload.code, history=payload.history, level="intermediate", @@ -23,7 +27,7 @@ async def chat(payload: ChatRequest) -> ChatResponse: pass fallback_reply = chat_fallback_reply( - message=payload.message, + message=prompt, code=payload.code, history=payload.history, level="beginner", @@ -33,10 +37,14 @@ async def chat(payload: ChatRequest) -> ChatResponse: @router.post("/message", response_model=ChatMessageResponse) async def chat_message(payload: ChatMessageRequest) -> ChatMessageResponse: + prompt = payload.message or "" + if payload.context: + prompt = f"{payload.context}\n\n{prompt}" if prompt else payload.context + if llm_analysis_client.enabled: try: reply = await llm_analysis_client.chat_reply( - message=payload.message, + message=prompt, code=payload.code, history=payload.history, level=payload.level, @@ -51,7 +59,7 @@ async def chat_message(payload: ChatMessageRequest) -> ChatMessageResponse: pass fallback_reply = chat_fallback_reply( - message=payload.message, + message=prompt, code=payload.code, history=payload.history, level=payload.level, diff --git a/backend/app/schemas.py b/backend/app/schemas.py index cfde6d91..817a2a47 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -311,13 +311,25 @@ class ShareRecord(BaseModel): # ── Chat ────────────────────────────────────────────────────────────────────── class ChatRequest(BaseModel): - message: str = Field(..., min_length=1, max_length=4_000) + message: str | None = Field(default=None, max_length=4_000) + question: str | None = Field(default=None, max_length=4_000) code: str | None = Field(default=None, max_length=settings.max_code_chars) + context: str | None = Field(default=None, max_length=8_000) + analysis_id: str | None = Field(default=None, max_length=200) history: list[str] = Field(default_factory=list, max_length=20) - @field_validator("message") + @field_validator("message", "question") @classmethod - def sanitize_message(cls, v: str) -> str: + def sanitize_optional_text(cls, v: str | None) -> str | None: + if v is None: + return None + return validate_stored_action(v) + + @field_validator("context") + @classmethod + def sanitize_context(cls, v: str | None) -> str | None: + if v is None: + return None return validate_stored_action(v) @field_validator("code") @@ -332,20 +344,40 @@ def sanitize_code(cls, v: str | None) -> str | None: def sanitize_history(cls, v: list[str]) -> list[str]: return validate_chat_history(v) + @model_validator(mode="after") + def ensure_prompt_present(self) -> "ChatRequest": + if not (self.message or self.question): + raise ValueError("message or question must be provided") + if not self.message and self.question: + self.message = self.question + return self + class ChatResponse(BaseModel): response: str class ChatMessageRequest(BaseModel): - message: str = Field(..., min_length=1, max_length=4_000) + message: str | None = Field(default=None, max_length=4_000) + question: str | None = Field(default=None, max_length=4_000) code: str | None = Field(default=None, max_length=settings.max_code_chars) + context: str | None = Field(default=None, max_length=8_000) + analysis_id: str | None = Field(default=None, max_length=200) history: list[str] = Field(default_factory=list, max_length=20) level: str = Field(default="beginner") - @field_validator("message") + @field_validator("message", "question") @classmethod - def sanitize_message(cls, v: str) -> str: + def sanitize_optional_text(cls, v: str | None) -> str | None: + if v is None: + return None + return validate_stored_action(v) + + @field_validator("context") + @classmethod + def sanitize_context(cls, v: str | None) -> str | None: + if v is None: + return None return validate_stored_action(v) @field_validator("code") @@ -365,6 +397,14 @@ def sanitize_history(cls, v: list[str]) -> list[str]: def sanitize_level(cls, v: str) -> str: return validate_stored_action(v) + @model_validator(mode="after") + def ensure_prompt_present(self) -> "ChatMessageRequest": + if not (self.message or self.question): + raise ValueError("message or question must be provided") + if not self.message and self.question: + self.message = self.question + return self + class ChatMessageResponse(BaseModel): provider: str diff --git a/backend/tests/test_chat.py b/backend/tests/test_chat.py new file mode 100644 index 00000000..ea74af0f --- /dev/null +++ b/backend/tests/test_chat.py @@ -0,0 +1,22 @@ +from fastapi.testclient import TestClient + +from app.main import app + +client = TestClient(app) + + +def test_chat_endpoint_uses_context_in_fallback_response(): + response = client.post( + "/chat/", + json={ + "message": "How do I fix this?", + "code": "def divide(a, b):\n return a / b\n", + "context": "This code divides by a runtime value and may fail with ZeroDivisionError.", + "history": [], + }, + ) + + assert response.status_code == 200 + body = response.json() + assert "response" in body + assert "division" in body["response"].lower() or "zerodivision" in body["response"].lower() diff --git a/frontend/index.html b/frontend/index.html index d625936e..05b6cfa4 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -252,6 +252,96 @@ gap: 8px } + .chat-panel { + border: 1px solid var(--border); + border-radius: var(--r); + padding: 12px; + background: var(--bg2); + box-shadow: var(--shadow2); + } + + .chat-panel-header { + display: flex; + align-items: center; + justify-content: space-between; + margin-bottom: 10px; + } + + .chat-panel-title { + font-weight: 700; + font-size: 0.95rem; + } + + .chat-panel-subtitle { + color: var(--text2); + font-size: 0.8rem; + margin-top: 2px; + } + + .chat-messages { + display: flex; + flex-direction: column; + gap: 8px; + min-height: 120px; + max-height: 220px; + overflow: auto; + padding: 6px 0 4px; + margin-bottom: 8px; + } + + .chat-empty { + color: var(--text2); + font-size: 0.9rem; + padding: 12px 4px; + } + + .chat-bubble { + padding: 8px 10px; + border-radius: 10px; + font-size: 0.9rem; + line-height: 1.45; + white-space: pre-wrap; + max-width: 100%; + } + + .chat-bubble.user { + background: var(--accent); + color: white; + align-self: flex-end; + } + + .chat-bubble.assistant { + background: var(--bg3); + color: var(--text); + align-self: flex-start; + border: 1px solid var(--border); + } + + .chat-input-row { + display: flex; + gap: 8px; + } + + .chat-input { + flex: 1; + border: 1px solid var(--border); + border-radius: 10px; + background: var(--bg); + color: var(--text); + padding: 10px 12px; + font: inherit; + } + + .chat-send { + border: none; + border-radius: 10px; + background: linear-gradient(135deg, var(--accent), var(--accent2)); + color: white; + font-weight: 600; + padding: 0 14px; + cursor: pointer; + } + .nav-link { display: flex; align-items: center; @@ -2417,6 +2507,22 @@

No suggestions yet

+
+
+
+
Ask follow-up questions
+
Use the current analysis as context
+
+
+
+
Ask something like “Why is this bug happening?”
+
+
+ + +
+
+ `; @@ -604,7 +713,7 @@ function renderResult(data, mode) { ${escHtml(i.type || 'Issue')} ${i.line ? `Line ${i.line}` : ''}

${escHtml(i.description || '')}

- ${i.suggestion ? `

→ ${escHtml(i.suggestion)}

` : ''} + ${getFixSuggestionMarkup(i)} `).join('')} `; diff --git a/frontend/style.css b/frontend/style.css index c4d1467a..e65f4552 100644 --- a/frontend/style.css +++ b/frontend/style.css @@ -579,6 +579,40 @@ body { border-color: rgba(59, 130, 246, 0.35); background: rgba(59, 130, 246, 0.12); } + +.fix-suggestion-row { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 10px; + margin-top: 6px; +} + +.fix-suggestion-text { + color: var(--accent-green); + font-size: 13px; + line-height: 1.5; +} + +.goto-line-btn { + border: 1px solid rgba(34, 197, 94, 0.25); + background: transparent; + color: var(--accent-green); + border-radius: 6px; + padding: 5px 10px; + font-family: var(--font-mono); + font-size: 12px; + cursor: pointer; + transition: background var(--transition), color var(--transition), transform var(--transition); +} + +.goto-line-btn:hover, +.goto-line-btn:focus-visible { + background: rgba(34, 197, 94, 0.12); + color: var(--text); + transform: translateY(-1px); +} + .engine-badge.llm { color: #86efac; border-color: rgba(34, 197, 94, 0.35); From d38cea623c9ad93e729aaf419d51b6786d16fa6d Mon Sep 17 00:00:00 2001 From: Void-Anvesha Date: Fri, 24 Jul 2026 12:51:48 +0530 Subject: [PATCH 5/6] fix-chat-code-correction --- CONTRIBUTING.md | 47 +++++++++++++++++++++ PR_BODY.md | 57 ++++++++++++++++++++++++++ backend/app/services/code_assistant.py | 47 +++++++++++++++++++-- backend/tests/test_code_assistant.py | 16 ++++++++ frontend/index.html | 5 ++- 5 files changed, 167 insertions(+), 5 deletions(-) create mode 100644 PR_BODY.md diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ca93a166..c520783a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -151,6 +151,53 @@ If you want, I can add a short `LLM_SETUP.md` with screenshots and copy-ready sn --- +## Large Files Policy + +CI automatically rejects PRs that contain files larger than **5 MB**. This keeps the repo lean and CI fast. + +### What to do if your PR fails + +- Check which files triggered the failure in the CI logs +- Remove the oversized files from the commit +- For large assets (screenshots, datasets, binaries), use **Git LFS** or an external hosting service and link to them in the README + +### Configure exceptions + +If you believe a file legitimately needs to exceed 5 MB (e.g., a bundled model or a large screenshot), add an exception to the check by modifying the comparison in `.github/workflows/check-large-files.yml`. + +--- + +## Code Formatting + +CI enforces consistent Python formatting on every pull request using `black` and `isort`. PRs with improperly formatted code will fail the `format` check automatically. + +### Install the tools + +```bash +cd backend +pip install black==24.10.0 isort==5.13.2 +``` + +### Format before every PR + +Run both from the repo root: + +```bash +black backend/ +isort backend/ +``` + +To check without modifying files (mirrors exactly what CI runs): + +```bash +black --check backend/ +isort --check-only backend/ +``` + +Both tools are pre-configured in `pyproject.toml` at the repo root so they stay compatible with each other — no manual flag juggling needed. + +--- + ## Pull Request Checklist Before opening a PR, confirm: diff --git a/PR_BODY.md b/PR_BODY.md new file mode 100644 index 00000000..d60fb20d --- /dev/null +++ b/PR_BODY.md @@ -0,0 +1,57 @@ +# PR Title +feat(chat): wire follow-up chat UI + add LLM setup docs + +## Description +Add a minimal follow-up chat handler to the frontend and document safe LLM/API-key setup for contributors. + +- Wire the chat Send button and Enter key in `frontend/index.html` so the chat pane sends POSTs to `/chat/`. +- Send richer chat payload (code, latest analysis/context, simple history) to help backend produce better replies. +- Add guidance to `CONTRIBUTING.md` describing how to create a local `backend/.env`, where to set `LLM_API_KEY`, and CI/deploy best-practices for secrets. +- Small housekeeping: expose a simple `window.chatHistory` to collect recent messages used as context. + +Notes: +- Default behavior remains rule-based when `LLM_ENABLED=false`. +- Ensure no secrets are committed: `.env` is ignored by `.gitignore`. If you accidentally committed `backend/.env`, remove it from the commit and rotate the key before opening this PR. + +## Related Issue +Fixes # + +## Type of change +- [ ] Bug fix +- [x] New feature / enhancement +- [x] Documentation update +- [ ] Test addition +- [ ] Refactor + +## Checklist +- [x] I have read [CONTRIBUTING.md](../CONTRIBUTING.md) +- [ ] My branch is up to date with `main` +- [ ] I have run `pytest -v` and all tests pass +- [x] I have not introduced duplicate issues or features +- [x] My PR title follows the format: `feat/fix/docs/test: short description` +- [ ] I have added tests for new features (Level 2 and 3 issues) +- [ ] No hardcoded secrets or API keys in my code (If `backend/.env` exists locally, ensure it's NOT staged for commit) +- [ ] This PR is linked to a GSSoC 2026 issue + +## Screenshots (if frontend change) +Before: n/a +After: chat input wired; Send/Enter submits; assistant replies appear in chat pane. + +## Test evidence +```bash +# Run backend locally and verify: +cd backend +python -m uvicorn app.main:app --reload + +# Then in browser open frontend/index.html (or use curl) +curl -s -X POST http://localhost:8000/chat/ -H "Content-Type: application/json" \ + -d '{"message":"Is there an issue with this code?","code":"print(hello);;","context":"","history":[]}' + +# Expected: backend responds (rule-based fallback or LLM reply if LLM enabled) +``` + +--- + +If you'd like, I can: +- Draft the PR title + description directly to clipboard, +- Or prepare a small follow-up commit to remove any accidental `backend/.env` from the index (i.e. `git rm --cached backend/.env`) and add a short `LLM_SETUP.md` for Render/GitHub Actions. Which should I do next? diff --git a/backend/app/services/code_assistant.py b/backend/app/services/code_assistant.py index 951853c6..ed628f52 100644 --- a/backend/app/services/code_assistant.py +++ b/backend/app/services/code_assistant.py @@ -204,7 +204,7 @@ def chat_fallback_reply( history: list[str], level: str, ) -> str: - """Return a simple fallback chat response when the LLM is unavailable.""" + """Return a useful code-aware reply when the LLM is unavailable.""" message_text = (message or "").strip() code_text = code or "" recent_history = " ".join(history[-3:]) if history else "" @@ -220,6 +220,47 @@ def chat_fallback_reply( language = detect_language(code_text) complexity = estimate_complexity(code_text) + + # Preserve useful debugging help when a live LLM is not configured. Python's + # parser can identify a syntax error and its line precisely. + asks_about_problem = bool( + re.search( + r"\b(issue|bug|error|wrong|fix|problem|fail(?:ing|ure)?)\b", + message_text, + re.IGNORECASE, + ) + ) + if language == "Python" and asks_about_problem: + try: + ast.parse(code_text) + except SyntaxError as exc: + line_number = exc.lineno or 1 + bad_line = code_text.splitlines()[line_number - 1].strip() + if bad_line.endswith(";;"): + corrected_line = bad_line.rstrip(";") + changes = [ + "Remove the extra semicolon (`;`).", + f"Use `{corrected_line}` instead of `{bad_line}`.", + ] + corrected_code = corrected_line + if corrected_line == "print(hello)": + changes.append("Add quotes around `hello` because it is text, not a variable.") + corrected_code = 'print("hello")' + else: + changes = ["Correct the invalid syntax on that line before running the code."] + corrected_code = bad_line + return "\n".join( + [ + "Issue found:", + f"- Line {line_number} has a Python syntax error: {exc.msg}.", + "", + "Changes to make:", + *[f"{index}. {change}" for index, change in enumerate(changes, start=1)], + "", + "Correct code:", + corrected_code, + ] + ) response_parts = [ f"I detected {language} code with an estimated {complexity.lower()} complexity.", f"At {level} level, focus on the main intent of the code and any notable branching or error-prone logic.", @@ -238,9 +279,7 @@ def chat_fallback_reply( ) if recent_history: - response_parts.append( - f"Recent chat context: {recent_history}." - ) + response_parts.append(f"Recent chat context: {recent_history}.") return " ".join(response_parts) diff --git a/backend/tests/test_code_assistant.py b/backend/tests/test_code_assistant.py index ec84d2ef..fdb3dc27 100644 --- a/backend/tests/test_code_assistant.py +++ b/backend/tests/test_code_assistant.py @@ -46,3 +46,19 @@ def test_chat_fallback_reply_for_error_query_suggests_common_issues() -> None: assert "common issues" in reply assert "incorrect indentation" in reply or "missing imports" in reply + + +def test_chat_fallback_reply_explains_python_syntax_error() -> None: + reply = chat_fallback_reply( + "What is the issue in the above code?", + "print(hello);;", + [], + "beginner", + ) + + assert "issue found" in reply.lower() + assert "line 1 has a python syntax error" in reply.lower() + assert "changes to make" in reply.lower() + assert "correct code" in reply.lower() + assert "extra semicolon" in reply.lower() + assert 'print("hello")' in reply diff --git a/frontend/index.html b/frontend/index.html index 05b6cfa4..954a66c5 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -3208,7 +3208,10 @@

No suggestions yet

function getApiBase() { const configuredBase = ((apiUrlInput && apiUrlInput.value) || localStorage.getItem('qyx_apiUrl') || '').trim(); if (configuredBase) return configuredBase.replace(/\/$/, ''); - return window.location.protocol === 'file:' ? REMOTE_API_BASE : LOCAL_API_BASE; + // Prefer the local API for the local HTML file so edits to this + // checkout are reflected immediately. fetchWithApiFallback still uses + // the deployed API when the local server is unavailable. + return LOCAL_API_BASE; } function getApiFallbackBase(base) { From 6c3e34bede540a17a0ba3f3cc20ebc8075321e8f Mon Sep 17 00:00:00 2001 From: Void-Anvesha Date: Sun, 26 Jul 2026 22:01:07 +0530 Subject: [PATCH 6/6] improve-syntax-suggestions --- backend/app/routers/analyze.py | 6 ++- backend/app/schemas.py | 1 + backend/app/services/code_assistant.py | 74 ++++++++++++++++++++++++++ backend/tests/test_endpoints.py | 35 ++++++++++++ frontend/index.html | 19 ++++++- 5 files changed, 131 insertions(+), 4 deletions(-) diff --git a/backend/app/routers/analyze.py b/backend/app/routers/analyze.py index 234a6873..9e2f4a0b 100644 --- a/backend/app/routers/analyze.py +++ b/backend/app/routers/analyze.py @@ -192,7 +192,9 @@ async def analyze_stream_get( ) async def analyze(req: CodeRequest, response: Response): cache_input = f"{req.language or 'auto'}\n{req.code}" - cached_payload = cache.get("analyze:v1", cache_input) + # Bump the namespace when analysis semantics change so results generated + # before syntax-first suggestions cannot be served as current analysis. + cached_payload = cache.get("analyze:v2", cache_input) if cached_payload is not None: response.headers["X-Cache"] = "HIT" @@ -200,7 +202,7 @@ async def analyze(req: CodeRequest, response: Response): payload = full_analysis(req.code, req.language) - cache.set("analyze:v1", cache_input, payload) + cache.set("analyze:v2", cache_input, payload) response.headers["X-Cache"] = "MISS" return payload diff --git a/backend/app/schemas.py b/backend/app/schemas.py index 817a2a47..3424d17c 100644 --- a/backend/app/schemas.py +++ b/backend/app/schemas.py @@ -76,6 +76,7 @@ class Suggestion(BaseModel): line_range: list[int] | None = None code_context: str | None = None example: str | None = None + example_type: str | None = None priority: str diff --git a/backend/app/services/code_assistant.py b/backend/app/services/code_assistant.py index ed628f52..9b0e34f0 100644 --- a/backend/app/services/code_assistant.py +++ b/backend/app/services/code_assistant.py @@ -8,6 +8,7 @@ import re import time from .ast_analyzer import analyze as ast_analyze +from .line_utils import format_code_snippet from dataclasses import dataclass, field # ── Language Detection ───────────────────────────────────────────────────────── @@ -914,6 +915,66 @@ def run_bug_detection(code: str, language: str) -> list[dict]: # ── Suggestion Engine ────────────────────────────────────────────────────────── +def _python_syntax_correction(code: str) -> dict | None: + """Return a safe, parseable correction for simple Python syntax mistakes.""" + try: + ast.parse(code) + return None + except SyntaxError as exc: + line_number = exc.lineno or 1 + + lines = code.splitlines() + if not 0 < line_number <= len(lines): + return None + + # Remove trailing semicolons and closing delimiters that do not have a + # matching opener. This covers common paste mistakes such as `print(1));;` + # without guessing at a rewrite of the user's program. + corrected_lines = lines.copy() + candidate = re.sub(r";+\s*$", "", corrected_lines[line_number - 1]) + opener_for = {")": "(", "]": "[", "}": "{"} + closer_for = {"(": ")", "[": "]", "{": "}"} + stack: list[str] = [] + corrected: list[str] = [] + for char in candidate: + if char in "([{": + stack.append(char) + elif char in opener_for: + if stack and stack[-1] == opener_for[char]: + stack.pop() + else: + continue + corrected.append(char) + # Complete delimiters left open on the invalid line (for example, + # `print(1` becomes `print(1)`). + corrected.extend(closer_for[opener] for opener in reversed(stack)) + corrected_lines[line_number - 1] = "".join(corrected) + corrected_code = "\n".join(corrected_lines) + + try: + ast.parse(corrected_code) + except SyntaxError: + return { + "category": "Syntax Error", + "description": f"Line {line_number} has invalid Python syntax. Fix it before applying other improvements.", + "line_number": line_number, + "line_range": [line_number], + "code_context": format_code_snippet(code, [line_number]), + "priority": "high", + } + + return { + "category": "Syntax Correction", + "description": f"Line {line_number} has invalid Python syntax. Fix it before applying other improvements.", + "line_number": line_number, + "line_range": [line_number], + "code_context": format_code_snippet(code, [line_number]), + "example": corrected_code, + "example_type": "fix", + "priority": "high", + } + + def run_suggestions(code: str, language: str) -> dict: """Generate improvement suggestions for the provided source code. @@ -936,6 +997,19 @@ def run_suggestions(code: str, language: str) -> dict: lines = code.splitlines() non_blank = [line for line in lines if line.strip()] + # A syntax error makes style-oriented suggestions misleading. Return a + # verified correction instead of generic documentation, test, or logging + # templates. + if language == "Python": + syntax_suggestion = _python_syntax_correction(code) + if syntax_suggestion: + return { + "suggestions": [syntax_suggestion], + "overall_score": 0, + "grade": "F", + "next_step": "Fix the syntax error, then run improvement analysis again.", + } + # ───────────────────────────────────────────────────────────── # SUGGESTION 1: Documentation Quality # ───────────────────────────────────────────────────────────── diff --git a/backend/tests/test_endpoints.py b/backend/tests/test_endpoints.py index c4d776be..f7f119fe 100644 --- a/backend/tests/test_endpoints.py +++ b/backend/tests/test_endpoints.py @@ -593,6 +593,41 @@ def test_suggestions_observability_print_only_python(): assert "Observability" in s_py +def test_suggestions_prioritize_a_verified_python_syntax_correction(): + r = client.post("/suggestions/", json={"code": "print(1));;", "language": "python"}) + + assert r.status_code == 200 + data = r.json() + assert data["overall_score"] == 0 + assert data["grade"] == "F" + assert len(data["suggestions"]) == 1 + + correction = data["suggestions"][0] + assert correction["category"] == "Syntax Correction" + assert correction["example"] == "print(1)" + assert correction["example_type"] == "fix" + + +def test_suggestions_fix_an_unclosed_python_delimiter_before_improving(): + r = client.post("/suggestions/", json={"code": "print(1", "language": "python"}) + + assert r.status_code == 200 + suggestions = r.json()["suggestions"] + assert len(suggestions) == 1 + assert suggestions[0]["category"] == "Syntax Correction" + assert suggestions[0]["example"] == "print(1)" + + +def test_full_analysis_does_not_return_generic_suggestions_for_syntax_errors(): + r = client.post("/analyze/", json={"code": "print(1", "language": "python"}) + + assert r.status_code == 200 + suggestions = r.json()["suggestions"]["suggestions"] + assert len(suggestions) == 1 + assert suggestions[0]["category"] == "Syntax Correction" + assert suggestions[0]["example"] == "print(1)" + + # ── Full Analysis ───────────────────────────────────────────────────────────── def test_full_analyze(): r = client.post("/analyze/", json={"code": PYTHON_BUGGY}) diff --git a/frontend/index.html b/frontend/index.html index 954a66c5..d82731c7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4069,6 +4069,19 @@

${getTranslation('debug_clean_title')}

/* ═══════════════════════════════════════════════════════════════ RENDER SUGGESTIONS (with diff view) ═══════════════════════════════════════════════════════════════ */ + function buildExampleHtml(example) { + const lines = example.split('\n').map((text, idx) => ({ type: 'added', text, num: idx + 1 })); + return ` +
+
+
+ Example +
+
+
${renderDiffCol(lines)}
+
`; + } + function renderSuggest(sugg) { document.getElementById('emptySuggest').style.display = 'none'; const el = document.getElementById('suggestResult'); @@ -4109,7 +4122,7 @@

${getTranslation('suggest_quality_score')}

: sugg.suggestions.map((s, i) => { let exampleHtml = ''; - if (s.example) { + if (s.example && s.example_type === 'fix') { const copyId = `copy-fix-${i}`; const rawLines = s.example.split('\n'); @@ -4153,6 +4166,8 @@

${getTranslation('suggest_quality_score')}

} } } + } else if (s.example) { + exampleHtml = buildExampleHtml(s.example); } return ` @@ -4170,7 +4185,7 @@

${getTranslation('suggest_quality_score')}

// Wire up "Copy fixed" buttons after DOM insertion sugg.suggestions.forEach((s, i) => { - if (!s.example) return; + if (!s.example || s.example_type !== 'fix') return; const btn = document.getElementById(`copy-fix-${i}`); if (!btn) return;