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 3d973ecf..c520783a 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,6 +1,6 @@ # Contributing to QyverixAI -Thank you for wanting to contribute! QyverixAI is a GSSoC 2026 project and welcomes all levels of contributors โ from first-timers to veterans. +Thank you for wanting to contribute! QyverixAI is a GSSoC 2026 project and welcomes all levels of contributors - from first-timers to veterans. --- @@ -19,7 +19,7 @@ git checkout -b feat/your-feature-name cd backend pip install -r requirements.txt -# 5. Run tests โ all must pass before submitting +# 5. Run tests - all must pass before submitting pytest -v # 6. Start the dev server @@ -30,11 +30,11 @@ uvicorn app.main:app --reload ## Ways to Contribute -### ๐ Bug Fixes +### Bug Fixes - Open an issue first if the bug isn't already reported - Include the code snippet that triggers it + expected vs actual behavior -### โจ New Bug Detection Patterns +### New Bug Detection Patterns Bug patterns live in `backend/app/services/code_assistant.py` in the `BUG_PATTERNS` list. Each pattern is a `BugPattern` dataclass: @@ -44,7 +44,7 @@ BugPattern( name="Pattern Name", pattern=r"regex_to_match", description="What the bug is and why it's a problem.", - suggestion="How to fix it โ be specific and actionable.", + suggestion="How to fix it - be specific and actionable.", severity="error", # "error" | "warning" | "info" languages=["Python"], # which languages this applies to ) @@ -60,18 +60,18 @@ def test_debug_detects_your_pattern(): assert "Pattern Name" in types ``` -### ๐ก New Suggestion Rules +### New Suggestion Rules Suggestion logic is in the `run_suggestions()` function in `code_assistant.py`. Add a new `if` block that appends to the `suggestions` list. -### ๐จ Frontend Improvements -The entire frontend is `frontend/index.html` โ one self-contained file. No build step, no Node.js required. Just edit and open in your browser. +### Frontend Improvements +The entire frontend is `frontend/index.html` - one self-contained file. No build step, no Node.js required. Just edit and open in your browser. -### ๐ Documentation +### Documentation - Fix typos, improve clarity, add examples - Update the README if you add/change a feature - Add docstrings to functions that lack them -### ๐งช Tests +### Tests - Add test cases for edge cases - Improve coverage for existing features - Parametrize tests where appropriate @@ -88,6 +88,116 @@ The entire frontend is `frontend/index.html` โ one self-contained file. No bui --- +## 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. + +--- + +## 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: @@ -115,4 +225,4 @@ Be respectful, inclusive, and constructive. We're here to learn and build togeth --- -Thank you for contributing! ๐ \ No newline at end of file +Thank you for contributing! diff --git a/FEATURE_LINE_HIGHLIGHTING.md b/FEATURE_LINE_HIGHLIGHTING.md new file mode 100644 index 00000000..1c68a932 --- /dev/null +++ b/FEATURE_LINE_HIGHLIGHTING.md @@ -0,0 +1,179 @@ +# Line Highlighting Feature - Implementation Complete โ + +## Feature Summary +Implemented visual line highlighting in the code editor to mark detected issue lines during code analysis. Issues are highlighted with color-coded severity levels and interactive hover tooltips. + +## What Was Enhanced + +### 1. **Visual Line Highlighting** +- Lines with detected issues are now highlighted directly in the code editor +- Highlights appear behind the text with semi-transparent backgrounds +- 3px left border accent for easy visibility +- Smooth opacity transitions on hover + +### 2. **Color-Coded Severity Levels** +- **Errors** (Red): `rgba(239, 68, 68, 0.15)` with `#ef4444` accent +- **Warnings** (Yellow): `rgba(234, 179, 8, 0.15)` with `#eab308` accent +- **Info** (Blue): `rgba(59, 130, 246, 0.15)` with `#3b82f6` accent + +### 3. **Interactive Tooltips** +- Hover over any highlighted line to reveal a tooltip +- Tooltip shows: Issue Type, Description, Fix Suggestion +- Styled to match the app's design with proper contrast + +### 4. **Automatic Lifecycle Management** +- **Applied**: When debug analysis completes +- **Updated**: Highest severity takes precedence if multiple issues on same line +- **Cleared**: When code is edited, new analysis runs, or Clear button is clicked + +## Files Modified + +### `frontend/style.css` +Added comprehensive styling for: +- `.editor-wrap` - Main editor container with line numbers +- `.line-numbers` - Line number gutter display +- `.code-editor-shell` - Container for highlights overlay +- `.issue-line-highlights` - Highlight decorator container +- `.issue-line-highlight` - Individual highlight decorations +- `.issue-tooltip` - Hover tooltip styling +- Light theme color scheme support + +### `frontend/script.js` +Added new functions: +- `applyLineHighlights(issues)` - Processes issues and creates visual decorations +- `clearLineHighlights()` - Removes all highlights from editor + +Updated existing code: +- Changed all `codeInput` references to `codeEditor` (sync with HTML) +- Integrated highlights clearing into: + - Input event listener (when code is edited) + - Clear button handler + - RunAnalysis startup + - Code upload handler + +### `frontend/index.html` +- Already had proper HTML structure prepared with `issue-line-highlights` div +- Line numbers gutter already present + +## How to Use + +1. **Paste or upload code** into the editor +2. **Select "Debug" mode** from the mode tabs or select "Full" for comprehensive analysis +3. **Click "Analyze Code"** button +4. **Visual highlights appear** on lines with detected issues +5. **Hover over any highlighted line** to see issue details +6. **Edit code** to automatically clear highlights +7. **Run new analysis** to update highlights + +## API Integration + +Backend returns issues with these fields (already supported): +```python +class Issue(BaseModel): + type: str # e.g., "SyntaxError", "NameError" + line: int | None # Line number where issue occurs + description: str # Detailed issue description + suggestion: str # How to fix the issue + severity: str # "error", "warning", or "info" + code_snippet: str # Optional code snippet + code_context: str # Optional additional context +``` + +## Technical Details + +### Z-Index Layering +- Highlights container: `z-index: 0` (behind text) +- Code editor textarea: `z-index: 1` (on top) +- Tooltips: `z-index: 1000` (above everything) + +### Line Height Calculation +- Line height: 1.7 (from CSS) +- Font size: 13px +- Highlight height: `calc(1.7 * 13px)` = 22.1px per line +- Padding adjustment: 16px top padding on editor + +### Hover Behavior +- Highlights have 0% opacity by default +- Hover reveals highlight at 60% opacity (class: `.active`) +- Tooltip displays on hover with full opacity +- Smooth transitions via `var(--transition)` (0.18s ease) + +### Theme Support +- Dark mode: Default colors (see above) +- Light theme: CSS variables adapt automatically +- All colors maintain proper contrast ratios + +## Edge Cases Handled + +1. **Multiple issues on same line** + - Only highest severity is highlighted + - Priority: error (3) > warning (2) > info (1) + +2. **Missing line numbers** + - Issues without line numbers are skipped + - Invalid line numbers (0 or negative) are ignored + +3. **Code editing** + - Highlights clear immediately on input + - Prevents stale highlights during editing + +4. **Theme switching** + - Highlights update with theme change + - Tooltip colors adapt to theme + +5. **Empty code** + - No highlights applied to empty editor + - Error message shown instead + +## Performance Considerations + +- Highlights use CSS transforms (GPU-accelerated) +- Tooltip creation deferred until hover +- Container innerHTML cleared instead of individual removals +- No scroll sync required (CSS positioned absolutely) + +## Browser Compatibility + +- Works with all modern browsers supporting: + - CSS Grid and Flexbox + - CSS Custom Properties (CSS Variables) + - ES6 JavaScript (const, arrow functions, template literals) + - Event listeners and DOM manipulation + +## Future Enhancement Ideas + +1. **Gutter Decorations** + - Add small icons in line number gutter for each issue + - Click to jump to issue details + +2. **Inline Code Actions** + - Quick fix buttons directly in highlights + - Apply suggestions with one click + +3. **Filter/Search Issues** + - Filter highlights by severity + - Search for specific issue types + +4. **Keyboard Navigation** + - Arrow keys to navigate between issues + - Keyboard shortcuts to apply fixes + +5. **Copy/Export** + - Export highlighted issues as report + - Copy highlighted section with annotations + +## Testing Checklist + +- [ ] Paste Python code with syntax errors and run Debug analysis +- [ ] Verify error lines are highlighted in red +- [ ] Hover over highlight to see tooltip +- [ ] Edit code and verify highlights clear +- [ ] Run new analysis to see updated highlights +- [ ] Test with warnings and info-level issues +- [ ] Verify light theme colors are visible and accessible +- [ ] Test with multiple issues on same line (should show highest severity) +- [ ] Test with code that has no issues (should show "No issues" message) +- [ ] Test keyboard shortcut (Ctrl+Enter) to run analysis + +## Status +โ **IMPLEMENTATION COMPLETE** - Ready for testing and deployment 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/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/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/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..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 @@ -311,13 +312,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 +345,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 +398,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/app/services/code_assistant.py b/backend/app/services/code_assistant.py index 951853c6..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 โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ @@ -204,7 +205,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 +221,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 +280,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) @@ -875,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. @@ -897,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_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/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/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/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 @@ + diff --git a/frontend/index.html b/frontend/index.html index 11ff77ac..d82731c7 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -4,6 +4,7 @@
+${escHtml(i.description || '')}
- ${i.suggestion ? `โ ${escHtml(i.suggestion)}
` : ''} + ${getFixSuggestionMarkup(i)} `).join('')} `; 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 += `