Spaces:
Sleeping
Sleeping
Commit ·
f2f397e
0
Parent(s):
project completed
Browse files- .github/workflows/generate_docs_action.py +136 -0
- .github/workflows/update-docs.yml +53 -0
- .gitignore +58 -0
- backend/doc_generator.py +456 -0
- backend/github_client.py +123 -0
- backend/main.py +427 -0
- backend/parser.py +55 -0
- backend/vector_store.py +225 -0
- docs/.gitkeep +0 -0
- frontend/.eslintrc.cjs +20 -0
- frontend/.gitignore +24 -0
- frontend/README.md +240 -0
- frontend/index.html +13 -0
- frontend/package-lock.json +0 -0
- frontend/package.json +32 -0
- frontend/postcss.config.js +6 -0
- frontend/public/vite.svg +1 -0
- frontend/src/App.css +0 -0
- frontend/src/App.jsx +737 -0
- frontend/src/assets/react.svg +1 -0
- frontend/src/index.css +113 -0
- frontend/src/main.jsx +10 -0
- frontend/tailwind.config.js +8 -0
- frontend/vite.config.js +7 -0
- requirements.txt +10 -0
.github/workflows/generate_docs_action.py
ADDED
|
@@ -0,0 +1,136 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#!/usr/bin/env python3
|
| 2 |
+
"""
|
| 3 |
+
GitHub Actions script to call DocWizard API and generate documentation.
|
| 4 |
+
Checks OpenRouter rate limits and skips if approaching daily limit.
|
| 5 |
+
"""
|
| 6 |
+
|
| 7 |
+
import os
|
| 8 |
+
import json
|
| 9 |
+
import sys
|
| 10 |
+
import time
|
| 11 |
+
from pathlib import Path
|
| 12 |
+
import requests
|
| 13 |
+
|
| 14 |
+
# Environment variables
|
| 15 |
+
OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
|
| 16 |
+
DOCWIZARD_API_URL = os.getenv('DOCWIZARD_API_URL')
|
| 17 |
+
GITHUB_REPOSITORY = os.getenv('GITHUB_REPOSITORY')
|
| 18 |
+
GITHUB_SERVER_URL = os.getenv('GITHUB_SERVER_URL', 'https://github.com')
|
| 19 |
+
|
| 20 |
+
# Validate required vars
|
| 21 |
+
if not OPENROUTER_API_KEY:
|
| 22 |
+
print("❌ Error: OPENROUTER_API_KEY secret not set")
|
| 23 |
+
sys.exit(1)
|
| 24 |
+
|
| 25 |
+
if not DOCWIZARD_API_URL:
|
| 26 |
+
print("❌ Error: DOCWIZARD_API_URL secret not set")
|
| 27 |
+
sys.exit(1)
|
| 28 |
+
|
| 29 |
+
repo_url = f"{GITHUB_SERVER_URL}/{GITHUB_REPOSITORY}"
|
| 30 |
+
print(f"🔍 Generating docs for: {repo_url}")
|
| 31 |
+
|
| 32 |
+
# Step 1: Check rate limits before proceeding
|
| 33 |
+
print("\n📊 Checking OpenRouter API rate limits...")
|
| 34 |
+
headers = {
|
| 35 |
+
'Authorization': f'Bearer {OPENROUTER_API_KEY}',
|
| 36 |
+
'HTTP-Referer': 'https://github.com/actions',
|
| 37 |
+
'X-Title': 'DocWizard-GitHubAction'
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
# Make a minimal request to check headers
|
| 41 |
+
try:
|
| 42 |
+
check_response = requests.post(
|
| 43 |
+
f"{DOCWIZARD_API_URL}/health",
|
| 44 |
+
headers=headers,
|
| 45 |
+
timeout=10
|
| 46 |
+
)
|
| 47 |
+
except Exception as e:
|
| 48 |
+
print(f"⚠️ Could not reach DocWizard API: {e}")
|
| 49 |
+
print("Skipping documentation generation")
|
| 50 |
+
sys.exit(0)
|
| 51 |
+
|
| 52 |
+
# Check rate limit headers from OpenRouter
|
| 53 |
+
remaining = check_response.headers.get('x-ratelimit-remaining-requests')
|
| 54 |
+
if remaining:
|
| 55 |
+
try:
|
| 56 |
+
remaining = int(remaining)
|
| 57 |
+
print(f"✓ Rate limit remaining: {remaining} requests")
|
| 58 |
+
|
| 59 |
+
if remaining < 10:
|
| 60 |
+
print(f"⚠️ Rate limit warning: Only {remaining} requests remaining today!")
|
| 61 |
+
print("Skipping documentation generation to preserve rate limit")
|
| 62 |
+
sys.exit(1) # Exit with error to trigger the notification step
|
| 63 |
+
except ValueError:
|
| 64 |
+
pass
|
| 65 |
+
|
| 66 |
+
# Step 2: Call generate-docs endpoint
|
| 67 |
+
print(f"\n🚀 Calling {DOCWIZARD_API_URL}/generate-docs...")
|
| 68 |
+
try:
|
| 69 |
+
response = requests.post(
|
| 70 |
+
f"{DOCWIZARD_API_URL}/generate-docs",
|
| 71 |
+
json={"repo_url": repo_url},
|
| 72 |
+
headers=headers,
|
| 73 |
+
timeout=600,
|
| 74 |
+
stream=True
|
| 75 |
+
)
|
| 76 |
+
except Exception as e:
|
| 77 |
+
print(f"❌ API request failed: {e}")
|
| 78 |
+
sys.exit(1)
|
| 79 |
+
|
| 80 |
+
if response.status_code != 200:
|
| 81 |
+
print(f"❌ API returned status {response.status_code}")
|
| 82 |
+
error_data = response.text[:200]
|
| 83 |
+
print(f"Response: {error_data}")
|
| 84 |
+
sys.exit(1)
|
| 85 |
+
|
| 86 |
+
# Step 3: Process streaming response and build documentation
|
| 87 |
+
print("\n📝 Processing documentation stream...")
|
| 88 |
+
docs_folder = Path("docs")
|
| 89 |
+
docs_folder.mkdir(exist_ok=True)
|
| 90 |
+
|
| 91 |
+
api_ref_path = docs_folder / "API_REFERENCE.md"
|
| 92 |
+
file_count = 0
|
| 93 |
+
|
| 94 |
+
with open(api_ref_path, 'w') as f:
|
| 95 |
+
f.write("# API Reference\n\n")
|
| 96 |
+
f.write(f"Auto-generated by DocWizard for: {repo_url}\n")
|
| 97 |
+
f.write(f"Generated at: {time.strftime('%Y-%m-%d %H:%M:%S UTC', time.gmtime())}\n\n")
|
| 98 |
+
f.write("---\n\n")
|
| 99 |
+
|
| 100 |
+
try:
|
| 101 |
+
for line in response.iter_lines():
|
| 102 |
+
if not line:
|
| 103 |
+
continue
|
| 104 |
+
|
| 105 |
+
try:
|
| 106 |
+
data = json.loads(line)
|
| 107 |
+
|
| 108 |
+
if data.get('status') == 'file_processed':
|
| 109 |
+
file_info = data.get('file', {})
|
| 110 |
+
filename = file_info.get('filename')
|
| 111 |
+
doc = file_info.get('documentation')
|
| 112 |
+
|
| 113 |
+
if doc:
|
| 114 |
+
f.write(f"## 📄 {filename}\n\n")
|
| 115 |
+
f.write(doc)
|
| 116 |
+
f.write("\n\n---\n\n")
|
| 117 |
+
file_count += 1
|
| 118 |
+
print(f" ✓ Processed {filename}")
|
| 119 |
+
|
| 120 |
+
elif data.get('status') == 'rate_limit':
|
| 121 |
+
print(f" ⚠️ {data.get('message')}")
|
| 122 |
+
|
| 123 |
+
elif data.get('status') == 'error':
|
| 124 |
+
print(f" ❌ Error: {data.get('message')}")
|
| 125 |
+
sys.exit(1)
|
| 126 |
+
|
| 127 |
+
except json.JSONDecodeError:
|
| 128 |
+
continue
|
| 129 |
+
|
| 130 |
+
except requests.exceptions.RequestException as e:
|
| 131 |
+
print(f"❌ Stream error: {e}")
|
| 132 |
+
sys.exit(1)
|
| 133 |
+
|
| 134 |
+
print(f"\n✅ Documentation generated successfully!")
|
| 135 |
+
print(f"📁 Saved to: {api_ref_path}")
|
| 136 |
+
print(f"📊 Total files processed: {file_count}")
|
.github/workflows/update-docs.yml
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
name: Update Documentation
|
| 2 |
+
|
| 3 |
+
on:
|
| 4 |
+
push:
|
| 5 |
+
branches:
|
| 6 |
+
- main
|
| 7 |
+
|
| 8 |
+
jobs:
|
| 9 |
+
update-docs:
|
| 10 |
+
runs-on: ubuntu-latest
|
| 11 |
+
steps:
|
| 12 |
+
- uses: actions/checkout@v3
|
| 13 |
+
with:
|
| 14 |
+
fetch-depth: 0
|
| 15 |
+
|
| 16 |
+
- name: Set up Python
|
| 17 |
+
uses: actions/setup-python@v4
|
| 18 |
+
with:
|
| 19 |
+
python-version: '3.11'
|
| 20 |
+
|
| 21 |
+
- name: Install dependencies
|
| 22 |
+
run: |
|
| 23 |
+
pip install -r requirements.txt
|
| 24 |
+
pip install requests
|
| 25 |
+
|
| 26 |
+
- name: Generate and update API reference
|
| 27 |
+
env:
|
| 28 |
+
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
|
| 29 |
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
| 30 |
+
DOCWIZARD_API_URL: ${{ secrets.DOCWIZARD_API_URL }}
|
| 31 |
+
run: |
|
| 32 |
+
python .github/workflows/generate_docs_action.py
|
| 33 |
+
|
| 34 |
+
- name: Commit and push changes
|
| 35 |
+
if: success()
|
| 36 |
+
run: |
|
| 37 |
+
git config --local user.email "action@github.com"
|
| 38 |
+
git config --local user.name "GitHub Actions Bot"
|
| 39 |
+
git add docs/API_REFERENCE.md
|
| 40 |
+
git commit -m "docs: update API reference [skip ci]" || echo "No changes to commit"
|
| 41 |
+
git push
|
| 42 |
+
|
| 43 |
+
- name: Report rate limit issue
|
| 44 |
+
if: failure()
|
| 45 |
+
uses: actions/github-script@v6
|
| 46 |
+
with:
|
| 47 |
+
script: |
|
| 48 |
+
github.rest.issues.createComment({
|
| 49 |
+
issue_number: context.issue.number,
|
| 50 |
+
owner: context.repo.owner,
|
| 51 |
+
repo: context.repo.repo,
|
| 52 |
+
body: '⚠️ **DocWizard Rate Limit**: OpenRouter API rate limit is near threshold. Documentation generation skipped. Please try again tomorrow.'
|
| 53 |
+
})
|
.gitignore
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Environment
|
| 2 |
+
.env
|
| 3 |
+
.env.local
|
| 4 |
+
.env.production
|
| 5 |
+
|
| 6 |
+
# Python
|
| 7 |
+
__pycache__/
|
| 8 |
+
*.pyc
|
| 9 |
+
*.pyo
|
| 10 |
+
*.pyd
|
| 11 |
+
.Python
|
| 12 |
+
*.egg-info/
|
| 13 |
+
dist/
|
| 14 |
+
build/
|
| 15 |
+
*.egg
|
| 16 |
+
|
| 17 |
+
# Virtual environment
|
| 18 |
+
venv/
|
| 19 |
+
env/
|
| 20 |
+
.venv/
|
| 21 |
+
|
| 22 |
+
# FAISS indexes (local data, don't commit)
|
| 23 |
+
faiss_indexes/
|
| 24 |
+
|
| 25 |
+
# Node / Frontend
|
| 26 |
+
node_modules/
|
| 27 |
+
frontend/dist/
|
| 28 |
+
frontend/build/
|
| 29 |
+
.npm
|
| 30 |
+
|
| 31 |
+
# Vite
|
| 32 |
+
*.local
|
| 33 |
+
|
| 34 |
+
# VS Code
|
| 35 |
+
.vscode/
|
| 36 |
+
*.code-workspace
|
| 37 |
+
|
| 38 |
+
# OS files
|
| 39 |
+
.DS_Store
|
| 40 |
+
Thumbs.db
|
| 41 |
+
desktop.ini
|
| 42 |
+
|
| 43 |
+
# Logs
|
| 44 |
+
*.log
|
| 45 |
+
logs/
|
| 46 |
+
npm-debug.log*
|
| 47 |
+
|
| 48 |
+
# Testing
|
| 49 |
+
.pytest_cache/
|
| 50 |
+
htmlcov/
|
| 51 |
+
.coverage
|
| 52 |
+
|
| 53 |
+
# Jupyter (if you ever experiment)
|
| 54 |
+
.ipynb_checkpoints/
|
| 55 |
+
|
| 56 |
+
# Sentence transformers cache (large model files)
|
| 57 |
+
.cache/
|
| 58 |
+
models/
|
backend/doc_generator.py
ADDED
|
@@ -0,0 +1,456 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import time
|
| 3 |
+
import requests
|
| 4 |
+
from typing import Dict, List, Any
|
| 5 |
+
from openai import OpenAI
|
| 6 |
+
from dotenv import load_dotenv
|
| 7 |
+
|
| 8 |
+
# Load .env file
|
| 9 |
+
load_dotenv()
|
| 10 |
+
|
| 11 |
+
# LLM model with environment variable override
|
| 12 |
+
MODEL = os.getenv("LLM_MODEL", "qwen/qwen3-coder:free")
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def generate_docs_for_repo(parsed_files: List[Dict[str, Any]]) -> Dict[str, str]:
|
| 16 |
+
"""
|
| 17 |
+
Generates Markdown documentation for an entire repository in a SINGLE API call.
|
| 18 |
+
|
| 19 |
+
Args:
|
| 20 |
+
parsed_files: List of dicts, each with keys:
|
| 21 |
+
- 'filename': str (the file path)
|
| 22 |
+
- 'parsed_data': dict with 'functions', 'classes', 'imports' from parser.py
|
| 23 |
+
|
| 24 |
+
Returns:
|
| 25 |
+
Dict mapping filename -> markdown documentation string
|
| 26 |
+
"""
|
| 27 |
+
if not parsed_files:
|
| 28 |
+
return {}
|
| 29 |
+
|
| 30 |
+
# Get API key from environment
|
| 31 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
| 32 |
+
if not api_key:
|
| 33 |
+
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
| 34 |
+
|
| 35 |
+
# Initialize OpenAI client with OpenRouter base URL
|
| 36 |
+
client = OpenAI(
|
| 37 |
+
api_key=api_key,
|
| 38 |
+
base_url='https://openrouter.ai/api/v1'
|
| 39 |
+
)
|
| 40 |
+
|
| 41 |
+
# Build ONE comprehensive prompt for all files
|
| 42 |
+
files_summary = []
|
| 43 |
+
for file_item in parsed_files:
|
| 44 |
+
filename = file_item['filename']
|
| 45 |
+
parsed_data = file_item['parsed_data']
|
| 46 |
+
|
| 47 |
+
functions_text = "\n".join([
|
| 48 |
+
f" - {func['name']}(): {func['docstring'] or 'No docstring'}"
|
| 49 |
+
for func in parsed_data.get('functions', [])
|
| 50 |
+
])
|
| 51 |
+
|
| 52 |
+
classes_text = "\n".join([
|
| 53 |
+
f" - {cls['name']}: {cls['docstring'] or 'No docstring'}"
|
| 54 |
+
for cls in parsed_data.get('classes', [])
|
| 55 |
+
])
|
| 56 |
+
|
| 57 |
+
imports_text = "\n".join([
|
| 58 |
+
f" - {imp}"
|
| 59 |
+
for imp in parsed_data.get('imports', [])
|
| 60 |
+
])
|
| 61 |
+
|
| 62 |
+
file_summary = f"""### File: {filename}
|
| 63 |
+
Functions:
|
| 64 |
+
{functions_text or " None"}
|
| 65 |
+
|
| 66 |
+
Classes:
|
| 67 |
+
{classes_text or " None"}
|
| 68 |
+
|
| 69 |
+
Imports:
|
| 70 |
+
{imports_text or " None"}
|
| 71 |
+
"""
|
| 72 |
+
files_summary.append(file_summary)
|
| 73 |
+
|
| 74 |
+
# Single comprehensive prompt with detailed formatting instructions
|
| 75 |
+
combined_prompt = f"""You are a professional code documentation expert. I have a Python repository with {len(parsed_files)} file(s) to document.
|
| 76 |
+
|
| 77 |
+
For EACH file below, write RICH, DETAILED Markdown documentation following this EXACT structure:
|
| 78 |
+
|
| 79 |
+
## filename.py
|
| 80 |
+
### Overview
|
| 81 |
+
[ONE LINE SUMMARY: What this file does in plain English]
|
| 82 |
+
|
| 83 |
+
### Key Components
|
| 84 |
+
[CREATE A TABLE with columns: Name | Type | Purpose | Parameters]
|
| 85 |
+
[Include ALL important functions and classes]
|
| 86 |
+
[Format: | function_name() | Function | Brief purpose | parameter_types |]
|
| 87 |
+
[Format: | ClassName | Class | Brief purpose | __init__ params |]
|
| 88 |
+
|
| 89 |
+
### Usage Example
|
| 90 |
+
[REALISTIC code example with actual variable names and imports]
|
| 91 |
+
[Show how to actually use the main components]
|
| 92 |
+
[Include multiple lines if needed]
|
| 93 |
+
|
| 94 |
+
### Notes
|
| 95 |
+
[Important gotchas, warnings, or tips about this file]
|
| 96 |
+
[Performance considerations if relevant]
|
| 97 |
+
[Dependencies or requirements if relevant]
|
| 98 |
+
|
| 99 |
+
---
|
| 100 |
+
|
| 101 |
+
IMPORTANT FORMATTING RULES:
|
| 102 |
+
1. Use ## for file headers ONLY
|
| 103 |
+
2. Use ### for section headers (Overview, Key Components, Usage Example, Notes)
|
| 104 |
+
3. For Key Components table, use markdown table format with pipes
|
| 105 |
+
4. Code examples must be in ```python code blocks
|
| 106 |
+
5. Always end with --- (three dashes) between files
|
| 107 |
+
6. Be verbose and helpful - this is for developers who need to understand code quickly
|
| 108 |
+
7. Include actual sample data/variable names in usage examples
|
| 109 |
+
8. Explain WHY and HOW, not just WHAT
|
| 110 |
+
|
| 111 |
+
=== REPOSITORY FILES ===
|
| 112 |
+
|
| 113 |
+
{''.join(files_summary)}
|
| 114 |
+
|
| 115 |
+
=== DOCUMENTATION ===
|
| 116 |
+
|
| 117 |
+
Now write comprehensive documentation for each file following the structure above exactly."""
|
| 118 |
+
|
| 119 |
+
# Make ONE API call for the entire repository
|
| 120 |
+
markdown_output = ""
|
| 121 |
+
retry_count = 0
|
| 122 |
+
max_retries = 1
|
| 123 |
+
|
| 124 |
+
while retry_count <= max_retries:
|
| 125 |
+
try:
|
| 126 |
+
stream = client.chat.completions.create(
|
| 127 |
+
model=MODEL,
|
| 128 |
+
messages=[
|
| 129 |
+
{
|
| 130 |
+
'role': 'user',
|
| 131 |
+
'content': combined_prompt
|
| 132 |
+
}
|
| 133 |
+
],
|
| 134 |
+
stream=True,
|
| 135 |
+
max_tokens=8000,
|
| 136 |
+
timeout=30,
|
| 137 |
+
extra_headers={
|
| 138 |
+
'HTTP-Referer': 'http://localhost:3000',
|
| 139 |
+
'X-Title': 'DocWizard'
|
| 140 |
+
}
|
| 141 |
+
)
|
| 142 |
+
|
| 143 |
+
# Collect all streamed chunks
|
| 144 |
+
for chunk in stream:
|
| 145 |
+
if chunk.choices[0].delta.content:
|
| 146 |
+
markdown_output += chunk.choices[0].delta.content
|
| 147 |
+
|
| 148 |
+
break # Success, exit retry loop
|
| 149 |
+
|
| 150 |
+
except requests.exceptions.ConnectionError as e:
|
| 151 |
+
print(f"❌ Cannot reach OpenRouter - check internet connection: {e}")
|
| 152 |
+
raise
|
| 153 |
+
|
| 154 |
+
except requests.exceptions.Timeout as e:
|
| 155 |
+
print(f"❌ Request timed out (30s): {e}")
|
| 156 |
+
raise
|
| 157 |
+
|
| 158 |
+
except requests.exceptions.HTTPError as e:
|
| 159 |
+
# Check for 429 (rate limit)
|
| 160 |
+
if e.response and e.response.status_code == 429:
|
| 161 |
+
if retry_count < max_retries:
|
| 162 |
+
print(f"⚠️ Rate limit hit (HTTP 429) - waiting 60 seconds before retry...")
|
| 163 |
+
time.sleep(60)
|
| 164 |
+
retry_count += 1
|
| 165 |
+
continue
|
| 166 |
+
else:
|
| 167 |
+
print(f"❌ Rate limit hit (HTTP 429) - max retries exhausted")
|
| 168 |
+
raise
|
| 169 |
+
else:
|
| 170 |
+
print(f"❌ HTTP Error {e.response.status_code if e.response else 'unknown'}: {e}")
|
| 171 |
+
raise
|
| 172 |
+
|
| 173 |
+
except Exception as e:
|
| 174 |
+
# Print full exception details so we can debug
|
| 175 |
+
print(f"❌ Unexpected error: {type(e).__name__}: {e}")
|
| 176 |
+
import traceback
|
| 177 |
+
print("Full traceback:")
|
| 178 |
+
traceback.print_exc()
|
| 179 |
+
raise
|
| 180 |
+
|
| 181 |
+
# Parse the response and split by filename
|
| 182 |
+
result = {}
|
| 183 |
+
current_filename = None
|
| 184 |
+
current_doc = ""
|
| 185 |
+
|
| 186 |
+
for line in markdown_output.split('\n'):
|
| 187 |
+
# Check if this is a file header (## filename or ### filename)
|
| 188 |
+
if line.startswith('##') and not line.startswith('###'):
|
| 189 |
+
# Save previous file if exists
|
| 190 |
+
if current_filename and current_doc.strip():
|
| 191 |
+
result[current_filename] = current_doc.strip()
|
| 192 |
+
|
| 193 |
+
# Extract filename from header
|
| 194 |
+
header_text = line.replace('##', '').strip()
|
| 195 |
+
# Try to match to an actual filename
|
| 196 |
+
for file_item in parsed_files:
|
| 197 |
+
if file_item['filename'] in header_text or header_text in file_item['filename']:
|
| 198 |
+
current_filename = file_item['filename']
|
| 199 |
+
current_doc = ""
|
| 200 |
+
break
|
| 201 |
+
else:
|
| 202 |
+
# Fallback: use the header text as-is
|
| 203 |
+
current_filename = header_text
|
| 204 |
+
current_doc = ""
|
| 205 |
+
elif current_filename:
|
| 206 |
+
current_doc += line + "\n"
|
| 207 |
+
|
| 208 |
+
# Save the last file
|
| 209 |
+
if current_filename and current_doc.strip():
|
| 210 |
+
result[current_filename] = current_doc.strip()
|
| 211 |
+
|
| 212 |
+
# Ensure all files have documentation (fallback to combined output if parsing failed)
|
| 213 |
+
if not result:
|
| 214 |
+
# If we couldn't parse individual files, return the combined output for all files
|
| 215 |
+
for file_item in parsed_files:
|
| 216 |
+
result[file_item['filename']] = markdown_output
|
| 217 |
+
else:
|
| 218 |
+
# For files without specific documentation, add a note
|
| 219 |
+
for file_item in parsed_files:
|
| 220 |
+
if file_item['filename'] not in result:
|
| 221 |
+
result[file_item['filename']] = f"Documentation for {file_item['filename']} is included in the comprehensive repository documentation above."
|
| 222 |
+
|
| 223 |
+
return result
|
| 224 |
+
|
| 225 |
+
|
| 226 |
+
def generate_readme_for_repo(repo_url: str, docs_list: List[Dict[str, str]]) -> str:
|
| 227 |
+
"""
|
| 228 |
+
Generates a comprehensive README.md for a repository using AI.
|
| 229 |
+
|
| 230 |
+
Args:
|
| 231 |
+
repo_url: The repository URL
|
| 232 |
+
docs_list: List of dicts with 'filename' and 'documentation' keys
|
| 233 |
+
|
| 234 |
+
Returns:
|
| 235 |
+
README markdown content as a string
|
| 236 |
+
"""
|
| 237 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
| 238 |
+
if not api_key:
|
| 239 |
+
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
| 240 |
+
|
| 241 |
+
client = OpenAI(
|
| 242 |
+
api_key=api_key,
|
| 243 |
+
base_url='https://openrouter.ai/api/v1'
|
| 244 |
+
)
|
| 245 |
+
|
| 246 |
+
# Extract project name from repo URL
|
| 247 |
+
project_name = repo_url.strip('/').split('/')[-1]
|
| 248 |
+
|
| 249 |
+
# Prepare documentation summary
|
| 250 |
+
docs_summary = "\n".join([
|
| 251 |
+
f"- {doc.get('filename', 'unknown')}: {doc.get('documentation', '')[:200]}..."
|
| 252 |
+
for doc in docs_list[:10] # Limit to first 10 files to stay within token limits
|
| 253 |
+
])
|
| 254 |
+
|
| 255 |
+
prompt = f"""You are a technical writer. Create a professional README.md for this Python project based on the documentation:
|
| 256 |
+
|
| 257 |
+
Project Repository: {repo_url}
|
| 258 |
+
Project Name: {project_name}
|
| 259 |
+
|
| 260 |
+
Generated File Documentations:
|
| 261 |
+
{docs_summary}
|
| 262 |
+
|
| 263 |
+
Create a comprehensive README.md that includes:
|
| 264 |
+
1. **Project Title and Description** - 2-3 sentences about what the project does
|
| 265 |
+
2. **Features** - A bulleted list of 5-7 key features
|
| 266 |
+
3. **Installation** - Python-specific installation steps (venv, pip-install pattern)
|
| 267 |
+
4. **Usage** - Example code snippets of how to use the project
|
| 268 |
+
5. **Project Structure** - Brief descriptions of the main files/directories
|
| 269 |
+
6. **Contributing** - Generic contributing guidelines
|
| 270 |
+
7. **License** - Standard license section (MIT suggested)
|
| 271 |
+
|
| 272 |
+
Format the output as proper Markdown. Make it professional and ready for GitHub."""
|
| 273 |
+
|
| 274 |
+
try:
|
| 275 |
+
stream = client.chat.completions.create(
|
| 276 |
+
model=MODEL,
|
| 277 |
+
messages=[
|
| 278 |
+
{
|
| 279 |
+
'role': 'user',
|
| 280 |
+
'content': prompt
|
| 281 |
+
}
|
| 282 |
+
],
|
| 283 |
+
stream=True,
|
| 284 |
+
max_tokens=2000,
|
| 285 |
+
timeout=30,
|
| 286 |
+
extra_headers={
|
| 287 |
+
'HTTP-Referer': 'http://localhost:3000',
|
| 288 |
+
'X-Title': 'DocWizard'
|
| 289 |
+
}
|
| 290 |
+
)
|
| 291 |
+
|
| 292 |
+
readme_content = ""
|
| 293 |
+
for chunk in stream:
|
| 294 |
+
if chunk.choices[0].delta.content:
|
| 295 |
+
readme_content += chunk.choices[0].delta.content
|
| 296 |
+
|
| 297 |
+
return readme_content.strip()
|
| 298 |
+
|
| 299 |
+
except requests.exceptions.ConnectionError as e:
|
| 300 |
+
raise ConnectionError(f"Cannot reach OpenRouter: {e}")
|
| 301 |
+
except requests.exceptions.Timeout as e:
|
| 302 |
+
raise TimeoutError(f"Request timed out (30s): {e}")
|
| 303 |
+
except requests.exceptions.HTTPError as e:
|
| 304 |
+
if e.response and e.response.status_code == 429:
|
| 305 |
+
raise Exception("Rate limit hit (HTTP 429) - please wait 60 seconds")
|
| 306 |
+
raise
|
| 307 |
+
|
| 308 |
+
|
| 309 |
+
def generate_gitignore_for_repo(repo_url: str, file_extensions: set, imports: set) -> str:
|
| 310 |
+
"""
|
| 311 |
+
Generates a .gitignore file for a repository based on project type.
|
| 312 |
+
|
| 313 |
+
Args:
|
| 314 |
+
repo_url: The repository URL
|
| 315 |
+
file_extensions: Set of file extensions found in the repo (e.g., {'.py', '.js', '.json'})
|
| 316 |
+
imports: Set of top-level imported modules
|
| 317 |
+
|
| 318 |
+
Returns:
|
| 319 |
+
.gitignore content as a string
|
| 320 |
+
"""
|
| 321 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
| 322 |
+
if not api_key:
|
| 323 |
+
raise ValueError("OPENROUTER_API_KEY environment variable not set")
|
| 324 |
+
|
| 325 |
+
client = OpenAI(
|
| 326 |
+
api_key=api_key,
|
| 327 |
+
base_url='https://openrouter.ai/api/v1'
|
| 328 |
+
)
|
| 329 |
+
|
| 330 |
+
# Detect project type based on file extensions and imports
|
| 331 |
+
project_types = []
|
| 332 |
+
if '.py' in file_extensions:
|
| 333 |
+
project_types.append('Python')
|
| 334 |
+
if '.js' in file_extensions or '.jsx' in file_extensions:
|
| 335 |
+
project_types.append('Node.js/JavaScript')
|
| 336 |
+
if '.ts' in file_extensions or '.tsx' in file_extensions:
|
| 337 |
+
project_types.append('TypeScript')
|
| 338 |
+
if '.go' in file_extensions:
|
| 339 |
+
project_types.append('Go')
|
| 340 |
+
if 'django' in imports or 'flask' in imports:
|
| 341 |
+
project_types.append('Web Framework (Django/Flask)')
|
| 342 |
+
if 'numpy' in imports or 'pandas' in imports or 'sklearn' in imports:
|
| 343 |
+
project_types.append('Data Science')
|
| 344 |
+
|
| 345 |
+
project_type_str = ', '.join(project_types) if project_types else 'Python'
|
| 346 |
+
|
| 347 |
+
prompt = f"""Generate a professional .gitignore file for this {project_type_str} project at {repo_url}.
|
| 348 |
+
|
| 349 |
+
Include these REQUIRED entries first:
|
| 350 |
+
.env
|
| 351 |
+
__pycache__/
|
| 352 |
+
*.pyc
|
| 353 |
+
venv/
|
| 354 |
+
node_modules/
|
| 355 |
+
faiss_indexes/
|
| 356 |
+
.DS_Store
|
| 357 |
+
|
| 358 |
+
Then add appropriate entries for this project type: {project_type_str}
|
| 359 |
+
|
| 360 |
+
Include entries for:
|
| 361 |
+
- Build artifacts and temporary files
|
| 362 |
+
- IDE and editor files (.vscode/, .idea/, *.swp)
|
| 363 |
+
- OS-specific files
|
| 364 |
+
- Virtual environment and dependency files
|
| 365 |
+
- Cache and database files
|
| 366 |
+
- Any framework-specific directories
|
| 367 |
+
|
| 368 |
+
Format as a standard .gitignore file with clear comments describing each section.
|
| 369 |
+
Return ONLY the .gitignore content, no explanations."""
|
| 370 |
+
|
| 371 |
+
try:
|
| 372 |
+
stream = client.chat.completions.create(
|
| 373 |
+
model=MODEL,
|
| 374 |
+
messages=[
|
| 375 |
+
{
|
| 376 |
+
'role': 'user',
|
| 377 |
+
'content': prompt
|
| 378 |
+
}
|
| 379 |
+
],
|
| 380 |
+
stream=True,
|
| 381 |
+
max_tokens=1500,
|
| 382 |
+
timeout=30,
|
| 383 |
+
extra_headers={
|
| 384 |
+
'HTTP-Referer': 'http://localhost:3000',
|
| 385 |
+
'X-Title': 'DocWizard'
|
| 386 |
+
}
|
| 387 |
+
)
|
| 388 |
+
|
| 389 |
+
gitignore_content = ""
|
| 390 |
+
for chunk in stream:
|
| 391 |
+
if chunk.choices[0].delta.content:
|
| 392 |
+
gitignore_content += chunk.choices[0].delta.content
|
| 393 |
+
|
| 394 |
+
return gitignore_content.strip()
|
| 395 |
+
|
| 396 |
+
except requests.exceptions.ConnectionError as e:
|
| 397 |
+
raise ConnectionError(f"Cannot reach OpenRouter: {e}")
|
| 398 |
+
except requests.exceptions.Timeout as e:
|
| 399 |
+
raise TimeoutError(f"Request timed out (30s): {e}")
|
| 400 |
+
except requests.exceptions.HTTPError as e:
|
| 401 |
+
if e.response and e.response.status_code == 429:
|
| 402 |
+
raise Exception("Rate limit hit (HTTP 429) - please wait 60 seconds")
|
| 403 |
+
raise
|
| 404 |
+
# Test script to debug OpenRouter connection
|
| 405 |
+
print("🧪 Testing OpenRouter API connection...")
|
| 406 |
+
print(f"OPENROUTER_API_KEY set: {'Yes' if os.getenv('OPENROUTER_API_KEY') else 'No'}")
|
| 407 |
+
print(f"MODEL: {MODEL}")
|
| 408 |
+
print()
|
| 409 |
+
|
| 410 |
+
api_key = os.getenv('OPENROUTER_API_KEY')
|
| 411 |
+
if not api_key:
|
| 412 |
+
print("❌ ERROR: OPENROUTER_API_KEY not found in environment!")
|
| 413 |
+
print(" Make sure .env file exists with OPENROUTER_API_KEY=your_key_here")
|
| 414 |
+
exit(1)
|
| 415 |
+
|
| 416 |
+
try:
|
| 417 |
+
print("📤 Sending test request to OpenRouter...")
|
| 418 |
+
response = requests.post(
|
| 419 |
+
"https://openrouter.ai/api/v1/chat/completions",
|
| 420 |
+
headers={
|
| 421 |
+
"Authorization": f"Bearer {api_key}",
|
| 422 |
+
"Content-Type": "application/json",
|
| 423 |
+
"HTTP-Referer": "http://localhost:3000",
|
| 424 |
+
"X-Title": "DocWizard-Test"
|
| 425 |
+
},
|
| 426 |
+
json={
|
| 427 |
+
"model": MODEL,
|
| 428 |
+
"messages": [{"role": "user", "content": "say hello"}],
|
| 429 |
+
"max_tokens": 10
|
| 430 |
+
},
|
| 431 |
+
timeout=30
|
| 432 |
+
)
|
| 433 |
+
|
| 434 |
+
print(f"\n✅ Status Code: {response.status_code}")
|
| 435 |
+
print(f"Response Headers: {dict(response.headers)}")
|
| 436 |
+
print(f"\nResponse Body:\n{response.text}\n")
|
| 437 |
+
|
| 438 |
+
if response.status_code == 200:
|
| 439 |
+
print("✅ Connection successful!")
|
| 440 |
+
elif response.status_code == 401:
|
| 441 |
+
print("❌ Unauthorized (401) - Check your OPENROUTER_API_KEY")
|
| 442 |
+
elif response.status_code == 429:
|
| 443 |
+
print("❌ Rate limited (429) - Too many requests")
|
| 444 |
+
else:
|
| 445 |
+
print(f"⚠️ Unexpected status code: {response.status_code}")
|
| 446 |
+
|
| 447 |
+
except requests.exceptions.ConnectionError as e:
|
| 448 |
+
print(f"❌ Connection Error: Cannot reach OpenRouter")
|
| 449 |
+
print(f" {e}")
|
| 450 |
+
except requests.exceptions.Timeout as e:
|
| 451 |
+
print(f"❌ Timeout Error: Request took longer than 30 seconds")
|
| 452 |
+
print(f" {e}")
|
| 453 |
+
except Exception as e:
|
| 454 |
+
print(f"❌ Error: {type(e).__name__}: {e}")
|
| 455 |
+
import traceback
|
| 456 |
+
traceback.print_exc()
|
backend/github_client.py
ADDED
|
@@ -0,0 +1,123 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import requests
|
| 3 |
+
from typing import List, Dict
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
def _parse_repo_url(repo_url: str) -> tuple:
|
| 7 |
+
"""
|
| 8 |
+
Parse GitHub URL and return owner and repo.
|
| 9 |
+
|
| 10 |
+
Args:
|
| 11 |
+
repo_url: GitHub URL in format "https://github.com/user/repo"
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Tuple of (owner, repo)
|
| 15 |
+
"""
|
| 16 |
+
parts = repo_url.rstrip('/').split('/')
|
| 17 |
+
owner = parts[-2]
|
| 18 |
+
repo = parts[-1]
|
| 19 |
+
return owner, repo
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _get_github_headers() -> Dict[str, str]:
|
| 23 |
+
"""
|
| 24 |
+
Get headers for GitHub API requests with authentication.
|
| 25 |
+
|
| 26 |
+
Returns:
|
| 27 |
+
Dict with Authorization header
|
| 28 |
+
"""
|
| 29 |
+
github_token = os.getenv('GITHUB_TOKEN')
|
| 30 |
+
if not github_token:
|
| 31 |
+
raise ValueError("GITHUB_TOKEN environment variable not set")
|
| 32 |
+
|
| 33 |
+
return {
|
| 34 |
+
'Authorization': f'token {github_token}',
|
| 35 |
+
'Accept': 'application/vnd.github.v3.raw'
|
| 36 |
+
}
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
def fetch_repo_files(repo_url: str) -> List[Dict[str, str]]:
|
| 40 |
+
"""
|
| 41 |
+
Fetches all Python files from a GitHub repository.
|
| 42 |
+
|
| 43 |
+
Args:
|
| 44 |
+
repo_url: GitHub URL in format "https://github.com/user/repo"
|
| 45 |
+
|
| 46 |
+
Returns:
|
| 47 |
+
List of dicts with keys 'filename' and 'content'
|
| 48 |
+
"""
|
| 49 |
+
owner, repo = _parse_repo_url(repo_url)
|
| 50 |
+
headers = _get_github_headers()
|
| 51 |
+
|
| 52 |
+
# Fetch the repository tree recursively
|
| 53 |
+
tree_url = f'https://api.github.com/repos/{owner}/{repo}/git/trees/main?recursive=1'
|
| 54 |
+
tree_response = requests.get(tree_url, headers=headers)
|
| 55 |
+
tree_response.raise_for_status()
|
| 56 |
+
tree_data = tree_response.json()
|
| 57 |
+
|
| 58 |
+
# Filter for Python files
|
| 59 |
+
py_files = [item for item in tree_data.get('tree', [])
|
| 60 |
+
if item['type'] == 'blob' and item['path'].endswith('.py')]
|
| 61 |
+
|
| 62 |
+
# Fetch content for each Python file
|
| 63 |
+
result = []
|
| 64 |
+
for file_item in py_files:
|
| 65 |
+
content_url = f'https://api.github.com/repos/{owner}/{repo}/contents/{file_item["path"]}'
|
| 66 |
+
content_response = requests.get(content_url, headers=headers)
|
| 67 |
+
content_response.raise_for_status()
|
| 68 |
+
|
| 69 |
+
result.append({
|
| 70 |
+
'filename': file_item['path'],
|
| 71 |
+
'content': content_response.text
|
| 72 |
+
})
|
| 73 |
+
|
| 74 |
+
return result
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
def get_changed_files(repo_url: str, since_commit: str) -> List[Dict[str, str]]:
|
| 78 |
+
"""
|
| 79 |
+
Fetches Python files changed since a given commit.
|
| 80 |
+
|
| 81 |
+
Args:
|
| 82 |
+
repo_url: GitHub URL in format "https://github.com/user/repo"
|
| 83 |
+
since_commit: Commit SHA to compare from (e.g., "abc123def456")
|
| 84 |
+
|
| 85 |
+
Returns:
|
| 86 |
+
List of dicts with keys 'filename' and 'content' for changed .py files
|
| 87 |
+
"""
|
| 88 |
+
owner, repo = _parse_repo_url(repo_url)
|
| 89 |
+
headers = _get_github_headers()
|
| 90 |
+
|
| 91 |
+
# Get comparison between since_commit and HEAD
|
| 92 |
+
compare_url = f'https://api.github.com/repos/{owner}/{repo}/compare/{since_commit}...HEAD'
|
| 93 |
+
compare_response = requests.get(compare_url, headers=headers)
|
| 94 |
+
compare_response.raise_for_status()
|
| 95 |
+
compare_data = compare_response.json()
|
| 96 |
+
|
| 97 |
+
# Extract changed files (filter for .py files)
|
| 98 |
+
files = compare_data.get('files', [])
|
| 99 |
+
changed_py_files = [
|
| 100 |
+
f for f in files
|
| 101 |
+
if f['filename'].endswith('.py') and f['status'] != 'removed'
|
| 102 |
+
]
|
| 103 |
+
|
| 104 |
+
if not changed_py_files:
|
| 105 |
+
return []
|
| 106 |
+
|
| 107 |
+
# Fetch content for each changed Python file
|
| 108 |
+
result = []
|
| 109 |
+
for file_item in changed_py_files:
|
| 110 |
+
content_url = f'https://api.github.com/repos/{owner}/{repo}/contents/{file_item["filename"]}'
|
| 111 |
+
try:
|
| 112 |
+
content_response = requests.get(content_url, headers=headers)
|
| 113 |
+
content_response.raise_for_status()
|
| 114 |
+
|
| 115 |
+
result.append({
|
| 116 |
+
'filename': file_item['filename'],
|
| 117 |
+
'content': content_response.text
|
| 118 |
+
})
|
| 119 |
+
except requests.exceptions.RequestException:
|
| 120 |
+
# File might have been deleted, skip it
|
| 121 |
+
continue
|
| 122 |
+
|
| 123 |
+
return result
|
backend/main.py
ADDED
|
@@ -0,0 +1,427 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import time
|
| 4 |
+
from fastapi import FastAPI
|
| 5 |
+
from fastapi.middleware.cors import CORSMiddleware
|
| 6 |
+
from fastapi.responses import StreamingResponse
|
| 7 |
+
from pydantic import BaseModel
|
| 8 |
+
from typing import AsyncGenerator, Dict, Any
|
| 9 |
+
|
| 10 |
+
from github_client import fetch_repo_files, get_changed_files
|
| 11 |
+
from parser import parse_python_file
|
| 12 |
+
from doc_generator import generate_docs_for_repo, generate_readme_for_repo, generate_gitignore_for_repo
|
| 13 |
+
from vector_store import store_docs, search_docs, update_docs
|
| 14 |
+
from dotenv import load_dotenv
|
| 15 |
+
load_dotenv()
|
| 16 |
+
|
| 17 |
+
app = FastAPI(title="DocWizard")
|
| 18 |
+
|
| 19 |
+
# Add CORS middleware for React frontend
|
| 20 |
+
app.add_middleware(
|
| 21 |
+
CORSMiddleware,
|
| 22 |
+
allow_origins=["http://localhost:5173", "http://localhost:3000"],
|
| 23 |
+
allow_credentials=True,
|
| 24 |
+
allow_methods=["*"],
|
| 25 |
+
allow_headers=["*"],
|
| 26 |
+
)
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
class GenerateDocsRequest(BaseModel):
|
| 30 |
+
repo_url: str
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
class AskRequest(BaseModel):
|
| 34 |
+
repo_url: str
|
| 35 |
+
question: str
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
class UpdateDocsRequest(BaseModel):
|
| 39 |
+
repo_url: str
|
| 40 |
+
since_commit: str
|
| 41 |
+
|
| 42 |
+
|
| 43 |
+
class GenerateReadmeRequest(BaseModel):
|
| 44 |
+
repo_url: str
|
| 45 |
+
|
| 46 |
+
|
| 47 |
+
class GenerateGitignoreRequest(BaseModel):
|
| 48 |
+
repo_url: str
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
@app.post("/generate-docs")
|
| 52 |
+
async def generate_docs(request: GenerateDocsRequest):
|
| 53 |
+
"""
|
| 54 |
+
Generates documentation for all Python files in a GitHub repository.
|
| 55 |
+
Makes ONE API call for the entire repository (efficient for free tier).
|
| 56 |
+
Streams results as newline-delimited JSON.
|
| 57 |
+
|
| 58 |
+
Args:
|
| 59 |
+
request: Contains repo_url like "https://github.com/user/repo"
|
| 60 |
+
|
| 61 |
+
Yields:
|
| 62 |
+
JSON objects for each file with filename, parsed_data, and documentation
|
| 63 |
+
"""
|
| 64 |
+
async def generate() -> AsyncGenerator[str, None]:
|
| 65 |
+
try:
|
| 66 |
+
# Fetch all Python files from GitHub
|
| 67 |
+
repo_files = fetch_repo_files(request.repo_url)
|
| 68 |
+
total_files = len(repo_files)
|
| 69 |
+
|
| 70 |
+
if total_files == 0:
|
| 71 |
+
yield json.dumps({
|
| 72 |
+
'status': 'error',
|
| 73 |
+
'message': 'No Python files found in repository',
|
| 74 |
+
'repo_url': request.repo_url
|
| 75 |
+
}) + '\n'
|
| 76 |
+
return
|
| 77 |
+
|
| 78 |
+
# Parse all files locally (no API calls)
|
| 79 |
+
parsed_files_with_names = []
|
| 80 |
+
for file_info in repo_files:
|
| 81 |
+
filename = file_info['filename']
|
| 82 |
+
content = file_info['content']
|
| 83 |
+
parsed_data = parse_python_file(filename, content)
|
| 84 |
+
parsed_files_with_names.append({
|
| 85 |
+
'filename': filename,
|
| 86 |
+
'parsed_data': parsed_data
|
| 87 |
+
})
|
| 88 |
+
|
| 89 |
+
# Generate documentation for ALL files in ONE API call
|
| 90 |
+
yield json.dumps({
|
| 91 |
+
'status': 'generating',
|
| 92 |
+
'message': 'Generating documentation for all files...',
|
| 93 |
+
'total_files': total_files
|
| 94 |
+
}) + '\n'
|
| 95 |
+
|
| 96 |
+
try:
|
| 97 |
+
docs_by_file = generate_docs_for_repo(parsed_files_with_names)
|
| 98 |
+
except Exception as e:
|
| 99 |
+
if '429' in str(e) or 'rate limit' in str(e).lower():
|
| 100 |
+
yield json.dumps({
|
| 101 |
+
'status': 'rate_limit',
|
| 102 |
+
'message': 'Rate limit hit — waiting 60 seconds...',
|
| 103 |
+
'total_files': total_files
|
| 104 |
+
}) + '\n'
|
| 105 |
+
time.sleep(60)
|
| 106 |
+
# Retry once
|
| 107 |
+
docs_by_file = generate_docs_for_repo(parsed_files_with_names)
|
| 108 |
+
else:
|
| 109 |
+
raise
|
| 110 |
+
|
| 111 |
+
# Stream the results
|
| 112 |
+
results = []
|
| 113 |
+
for idx, file_item in enumerate(parsed_files_with_names):
|
| 114 |
+
filename = file_item['filename']
|
| 115 |
+
parsed_data = file_item['parsed_data']
|
| 116 |
+
documentation = docs_by_file.get(filename, 'No documentation generated')
|
| 117 |
+
|
| 118 |
+
result = {
|
| 119 |
+
'filename': filename,
|
| 120 |
+
'parsed_data': parsed_data,
|
| 121 |
+
'documentation': documentation
|
| 122 |
+
}
|
| 123 |
+
results.append(result)
|
| 124 |
+
|
| 125 |
+
# Yield progress update with result
|
| 126 |
+
yield json.dumps({
|
| 127 |
+
'status': 'file_processed',
|
| 128 |
+
'current': idx + 1,
|
| 129 |
+
'total': total_files,
|
| 130 |
+
'file': result
|
| 131 |
+
}) + '\n'
|
| 132 |
+
|
| 133 |
+
# Save all docs to FAISS index BEFORE returning response
|
| 134 |
+
docs_for_storage = [
|
| 135 |
+
{'filename': r['filename'], 'documentation': r['documentation']}
|
| 136 |
+
for r in results
|
| 137 |
+
]
|
| 138 |
+
store_docs(request.repo_url, docs_for_storage)
|
| 139 |
+
|
| 140 |
+
# Final success message
|
| 141 |
+
yield json.dumps({
|
| 142 |
+
'status': 'complete',
|
| 143 |
+
'total_files': total_files,
|
| 144 |
+
'repo_url': request.repo_url,
|
| 145 |
+
'message': f'Generated documentation for {total_files} files in 1 API call'
|
| 146 |
+
}) + '\n'
|
| 147 |
+
|
| 148 |
+
except Exception as e:
|
| 149 |
+
yield json.dumps({
|
| 150 |
+
'status': 'error',
|
| 151 |
+
'message': str(e),
|
| 152 |
+
'repo_url': request.repo_url
|
| 153 |
+
}) + '\n'
|
| 154 |
+
|
| 155 |
+
return StreamingResponse(generate(), media_type='application/x-ndjson')
|
| 156 |
+
|
| 157 |
+
|
| 158 |
+
@app.post("/ask")
|
| 159 |
+
async def ask_question(request: AskRequest) -> Dict[str, Any]:
|
| 160 |
+
"""
|
| 161 |
+
Searches stored documentation for answers to a question.
|
| 162 |
+
|
| 163 |
+
Args:
|
| 164 |
+
request: Contains repo_url and question
|
| 165 |
+
|
| 166 |
+
Returns:
|
| 167 |
+
Dict with search results containing document chunks and filenames
|
| 168 |
+
"""
|
| 169 |
+
try:
|
| 170 |
+
results = search_docs(request.repo_url, request.question)
|
| 171 |
+
|
| 172 |
+
return {
|
| 173 |
+
'status': 'success',
|
| 174 |
+
'repo_url': request.repo_url,
|
| 175 |
+
'question': request.question,
|
| 176 |
+
'results': results,
|
| 177 |
+
'count': len(results)
|
| 178 |
+
}
|
| 179 |
+
|
| 180 |
+
except Exception as e:
|
| 181 |
+
return {
|
| 182 |
+
'status': 'error',
|
| 183 |
+
'message': str(e),
|
| 184 |
+
'repo_url': request.repo_url
|
| 185 |
+
}
|
| 186 |
+
|
| 187 |
+
|
| 188 |
+
@app.post("/update-docs")
|
| 189 |
+
async def update_docs_endpoint(request: UpdateDocsRequest):
|
| 190 |
+
"""
|
| 191 |
+
Updates documentation for files changed since a specific commit.
|
| 192 |
+
Streams results as newline-delimited JSON.
|
| 193 |
+
|
| 194 |
+
Args:
|
| 195 |
+
request: Contains repo_url and since_commit SHA
|
| 196 |
+
|
| 197 |
+
Yields:
|
| 198 |
+
JSON objects for each processed file with filename, parsed_data, and documentation
|
| 199 |
+
"""
|
| 200 |
+
async def generate() -> AsyncGenerator[str, None]:
|
| 201 |
+
try:
|
| 202 |
+
# Step 1: Get changed files since commit
|
| 203 |
+
changed_files = get_changed_files(request.repo_url, request.since_commit)
|
| 204 |
+
|
| 205 |
+
if not changed_files:
|
| 206 |
+
yield json.dumps({
|
| 207 |
+
'status': 'no_changes',
|
| 208 |
+
'message': 'No Python files changed since the specified commit',
|
| 209 |
+
'repo_url': request.repo_url,
|
| 210 |
+
'since_commit': request.since_commit
|
| 211 |
+
}) + '\n'
|
| 212 |
+
return
|
| 213 |
+
|
| 214 |
+
total_files = len(changed_files)
|
| 215 |
+
|
| 216 |
+
# Parse all changed files locally
|
| 217 |
+
parsed_files_with_names = []
|
| 218 |
+
for file_info in changed_files:
|
| 219 |
+
filename = file_info['filename']
|
| 220 |
+
content = file_info['content']
|
| 221 |
+
parsed_data = parse_python_file(filename, content)
|
| 222 |
+
parsed_files_with_names.append({
|
| 223 |
+
'filename': filename,
|
| 224 |
+
'parsed_data': parsed_data
|
| 225 |
+
})
|
| 226 |
+
|
| 227 |
+
# Generate documentation for ALL changed files in ONE API call
|
| 228 |
+
yield json.dumps({
|
| 229 |
+
'status': 'generating',
|
| 230 |
+
'message': f'Generating documentation for {total_files} changed files...',
|
| 231 |
+
'total_files': total_files
|
| 232 |
+
}) + '\n'
|
| 233 |
+
|
| 234 |
+
try:
|
| 235 |
+
docs_by_file = generate_docs_for_repo(parsed_files_with_names)
|
| 236 |
+
except Exception as e:
|
| 237 |
+
if '429' in str(e) or 'rate limit' in str(e).lower():
|
| 238 |
+
yield json.dumps({
|
| 239 |
+
'status': 'rate_limit',
|
| 240 |
+
'message': 'Rate limit hit — waiting 60 seconds...',
|
| 241 |
+
'total_files': total_files
|
| 242 |
+
}) + '\n'
|
| 243 |
+
time.sleep(60)
|
| 244 |
+
# Retry once
|
| 245 |
+
docs_by_file = generate_docs_for_repo(parsed_files_with_names)
|
| 246 |
+
else:
|
| 247 |
+
raise
|
| 248 |
+
|
| 249 |
+
# Stream the results
|
| 250 |
+
results = []
|
| 251 |
+
for idx, file_item in enumerate(parsed_files_with_names):
|
| 252 |
+
filename = file_item['filename']
|
| 253 |
+
parsed_data = file_item['parsed_data']
|
| 254 |
+
documentation = docs_by_file.get(filename, 'No documentation generated')
|
| 255 |
+
|
| 256 |
+
result = {
|
| 257 |
+
'filename': filename,
|
| 258 |
+
'parsed_data': parsed_data,
|
| 259 |
+
'documentation': documentation
|
| 260 |
+
}
|
| 261 |
+
results.append(result)
|
| 262 |
+
|
| 263 |
+
# Yield progress update with result
|
| 264 |
+
yield json.dumps({
|
| 265 |
+
'status': 'file_processed',
|
| 266 |
+
'current': idx + 1,
|
| 267 |
+
'total': total_files,
|
| 268 |
+
'file': result
|
| 269 |
+
}) + '\n'
|
| 270 |
+
|
| 271 |
+
# Update FAISS index with changed files
|
| 272 |
+
update_docs(request.repo_url, results)
|
| 273 |
+
|
| 274 |
+
# Final success message
|
| 275 |
+
yield json.dumps({
|
| 276 |
+
'status': 'complete',
|
| 277 |
+
'total_files': total_files,
|
| 278 |
+
'repo_url': request.repo_url,
|
| 279 |
+
'since_commit': request.since_commit,
|
| 280 |
+
'message': f'Updated {total_files} changed files in FAISS index (1 API call)'
|
| 281 |
+
}) + '\n'
|
| 282 |
+
|
| 283 |
+
except Exception as e:
|
| 284 |
+
yield json.dumps({
|
| 285 |
+
'status': 'error',
|
| 286 |
+
'message': str(e),
|
| 287 |
+
'repo_url': request.repo_url,
|
| 288 |
+
'since_commit': request.since_commit
|
| 289 |
+
}) + '\n'
|
| 290 |
+
|
| 291 |
+
return StreamingResponse(generate(), media_type='application/x-ndjson')
|
| 292 |
+
|
| 293 |
+
|
| 294 |
+
@app.get("/health")
|
| 295 |
+
async def health_check():
|
| 296 |
+
"""Health check endpoint."""
|
| 297 |
+
return {'status': 'ok'}
|
| 298 |
+
|
| 299 |
+
|
| 300 |
+
@app.post("/generate-readme")
|
| 301 |
+
async def generate_readme(request: GenerateReadmeRequest) -> Dict[str, Any]:
|
| 302 |
+
"""
|
| 303 |
+
Generates a comprehensive README.md for a repository.
|
| 304 |
+
Uses already-generated documentation from FAISS index.
|
| 305 |
+
|
| 306 |
+
Args:
|
| 307 |
+
request: Contains repo_url
|
| 308 |
+
|
| 309 |
+
Returns:
|
| 310 |
+
Dict with status and readme markdown content
|
| 311 |
+
"""
|
| 312 |
+
try:
|
| 313 |
+
# Search FAISS for all docs (use a broad query or get all with a dummy search)
|
| 314 |
+
# For now, we'll search for a generic term that should match most documentation
|
| 315 |
+
print(f"[GENERATE_README] Fetching docs for {request.repo_url} from FAISS...")
|
| 316 |
+
results = search_docs(request.repo_url, "overview project structure functions classes", num_results=100)
|
| 317 |
+
|
| 318 |
+
# Convert search results to the format generate_readme_for_repo expects
|
| 319 |
+
# Extract unique documents
|
| 320 |
+
seen_docs = set()
|
| 321 |
+
docs_list = []
|
| 322 |
+
|
| 323 |
+
print(f"[GENERATE_README] Retrieved {len(results)} result chunks, extracting unique docs...")
|
| 324 |
+
for result in results:
|
| 325 |
+
filename = result.get('filename', 'unknown')
|
| 326 |
+
if filename not in seen_docs:
|
| 327 |
+
docs_list.append({
|
| 328 |
+
'filename': filename,
|
| 329 |
+
'documentation': result.get('document', '')
|
| 330 |
+
})
|
| 331 |
+
seen_docs.add(filename)
|
| 332 |
+
|
| 333 |
+
if not docs_list:
|
| 334 |
+
print(f"[GENERATE_README] No docs found for {request.repo_url} - generating README anyway")
|
| 335 |
+
# Fallback: create README with repo info
|
| 336 |
+
readme_content = f"""# {request.repo_url.split('/')[-1]}
|
| 337 |
+
|
| 338 |
+
This project needs documentation. Please generate docs first using the /generate-docs endpoint.
|
| 339 |
+
"""
|
| 340 |
+
return {
|
| 341 |
+
'status': 'success',
|
| 342 |
+
'readme': readme_content,
|
| 343 |
+
'repo_url': request.repo_url,
|
| 344 |
+
'message': 'Generated README (note: docs not found - please generate docs first)'
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
print(f"[GENERATE_README] Calling generate_readme_for_repo with {len(docs_list)} docs...")
|
| 348 |
+
readme_content = generate_readme_for_repo(request.repo_url, docs_list)
|
| 349 |
+
|
| 350 |
+
print(f"[GENERATE_README] README generated successfully ({len(readme_content)} chars)")
|
| 351 |
+
return {
|
| 352 |
+
'status': 'success',
|
| 353 |
+
'readme': readme_content,
|
| 354 |
+
'repo_url': request.repo_url,
|
| 355 |
+
'message': 'README generated successfully'
|
| 356 |
+
}
|
| 357 |
+
|
| 358 |
+
except Exception as e:
|
| 359 |
+
print(f"[GENERATE_README] Error: {e}")
|
| 360 |
+
return {
|
| 361 |
+
'status': 'error',
|
| 362 |
+
'message': str(e),
|
| 363 |
+
'repo_url': request.repo_url
|
| 364 |
+
}
|
| 365 |
+
|
| 366 |
+
|
| 367 |
+
@app.post("/generate-gitignore")
|
| 368 |
+
async def generate_gitignore(request: GenerateGitignoreRequest) -> Dict[str, Any]:
|
| 369 |
+
"""
|
| 370 |
+
Generates a .gitignore file for a repository.
|
| 371 |
+
Analyzes file extensions and imports to detect project type.
|
| 372 |
+
|
| 373 |
+
Args:
|
| 374 |
+
request: Contains repo_url
|
| 375 |
+
|
| 376 |
+
Returns:
|
| 377 |
+
Dict with status and gitignore content
|
| 378 |
+
"""
|
| 379 |
+
try:
|
| 380 |
+
print(f"[GENERATE_GITIGNORE] Fetching repo files for {request.repo_url}...")
|
| 381 |
+
|
| 382 |
+
# Fetch repository files
|
| 383 |
+
repo_files = fetch_repo_files(request.repo_url)
|
| 384 |
+
|
| 385 |
+
# Analyze file extensions and imports
|
| 386 |
+
file_extensions = set()
|
| 387 |
+
all_imports = set()
|
| 388 |
+
|
| 389 |
+
for file_info in repo_files:
|
| 390 |
+
filename = file_info['filename']
|
| 391 |
+
content = file_info['content']
|
| 392 |
+
|
| 393 |
+
# Extract file extension
|
| 394 |
+
if '.' in filename:
|
| 395 |
+
ext = filename[filename.rfind('.'):]
|
| 396 |
+
file_extensions.add(ext)
|
| 397 |
+
|
| 398 |
+
# Parse file to extract imports
|
| 399 |
+
if filename.endswith('.py'):
|
| 400 |
+
parsed_data = parse_python_file(filename, content)
|
| 401 |
+
for imp in parsed_data.get('imports', []):
|
| 402 |
+
# Extract just the module name
|
| 403 |
+
module_name = imp.split()[1].split('.')[0] if len(imp.split()) > 1 else imp.split('.')[0]
|
| 404 |
+
all_imports.add(module_name.lower())
|
| 405 |
+
|
| 406 |
+
print(f"[GENERATE_GITIGNORE] Found extensions: {file_extensions}")
|
| 407 |
+
print(f"[GENERATE_GITIGNORE] Found imports: {all_imports}")
|
| 408 |
+
|
| 409 |
+
# Generate gitignore
|
| 410 |
+
print(f"[GENERATE_GITIGNORE] Calling generate_gitignore_for_repo...")
|
| 411 |
+
gitignore_content = generate_gitignore_for_repo(request.repo_url, file_extensions, all_imports)
|
| 412 |
+
|
| 413 |
+
print(f"[GENERATE_GITIGNORE] .gitignore generated successfully ({len(gitignore_content)} chars)")
|
| 414 |
+
return {
|
| 415 |
+
'status': 'success',
|
| 416 |
+
'gitignore': gitignore_content,
|
| 417 |
+
'repo_url': request.repo_url,
|
| 418 |
+
'message': '.gitignore generated successfully'
|
| 419 |
+
}
|
| 420 |
+
|
| 421 |
+
except Exception as e:
|
| 422 |
+
print(f"[GENERATE_GITIGNORE] Error: {e}")
|
| 423 |
+
return {
|
| 424 |
+
'status': 'error',
|
| 425 |
+
'message': str(e),
|
| 426 |
+
'repo_url': request.repo_url
|
| 427 |
+
}
|
backend/parser.py
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import ast
|
| 2 |
+
from typing import Dict, List, Any
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
def parse_python_file(filename: str, content: str) -> Dict[str, List[Any]]:
|
| 6 |
+
"""
|
| 7 |
+
Parses a Python file and extracts functions, classes, and imports.
|
| 8 |
+
|
| 9 |
+
Args:
|
| 10 |
+
filename: The name of the Python file
|
| 11 |
+
content: The content of the Python file as a string
|
| 12 |
+
|
| 13 |
+
Returns:
|
| 14 |
+
Dict with keys 'functions', 'classes', and 'imports'.
|
| 15 |
+
Each function/class dict contains 'name' and 'docstring'.
|
| 16 |
+
"""
|
| 17 |
+
try:
|
| 18 |
+
tree = ast.parse(content)
|
| 19 |
+
except SyntaxError as e:
|
| 20 |
+
print(f"Syntax error in {filename}: {e}")
|
| 21 |
+
return {'functions': [], 'classes': [], 'imports': []}
|
| 22 |
+
|
| 23 |
+
functions = []
|
| 24 |
+
classes = []
|
| 25 |
+
imports = []
|
| 26 |
+
|
| 27 |
+
for node in ast.walk(tree):
|
| 28 |
+
if isinstance(node, ast.FunctionDef):
|
| 29 |
+
docstring = ast.get_docstring(node)
|
| 30 |
+
functions.append({
|
| 31 |
+
'name': node.name,
|
| 32 |
+
'docstring': docstring
|
| 33 |
+
})
|
| 34 |
+
elif isinstance(node, ast.ClassDef):
|
| 35 |
+
docstring = ast.get_docstring(node)
|
| 36 |
+
classes.append({
|
| 37 |
+
'name': node.name,
|
| 38 |
+
'docstring': docstring
|
| 39 |
+
})
|
| 40 |
+
elif isinstance(node, ast.Import):
|
| 41 |
+
for alias in node.names:
|
| 42 |
+
imports.append(alias.name)
|
| 43 |
+
elif isinstance(node, ast.ImportFrom):
|
| 44 |
+
module = node.module or ''
|
| 45 |
+
for alias in node.names:
|
| 46 |
+
if module:
|
| 47 |
+
imports.append(f"from {module} import {alias.name}")
|
| 48 |
+
else:
|
| 49 |
+
imports.append(f"import {alias.name}")
|
| 50 |
+
|
| 51 |
+
return {
|
| 52 |
+
'functions': functions,
|
| 53 |
+
'classes': classes,
|
| 54 |
+
'imports': imports
|
| 55 |
+
}
|
backend/vector_store.py
ADDED
|
@@ -0,0 +1,225 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import os
|
| 2 |
+
import json
|
| 3 |
+
import faiss
|
| 4 |
+
import numpy as np
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from typing import List, Dict, Any
|
| 7 |
+
from sentence_transformers import SentenceTransformer
|
| 8 |
+
|
| 9 |
+
# Initialize embedding model
|
| 10 |
+
model = SentenceTransformer('all-MiniLM-L6-v2')
|
| 11 |
+
|
| 12 |
+
# Directory to store FAISS indexes
|
| 13 |
+
INDEXES_DIR = Path('faiss_indexes')
|
| 14 |
+
INDEXES_DIR.mkdir(exist_ok=True)
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
def _sanitize_repo_url(repo_url: str) -> str:
|
| 18 |
+
"""Sanitize repository URL to create a valid filename."""
|
| 19 |
+
return repo_url.replace('https://', '').replace('http://', '').replace('/', '_').replace('.', '_').replace(':', '_')[:63]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def store_docs(repo_url: str, docs_list: List[Dict[str, Any]]) -> None:
|
| 23 |
+
"""
|
| 24 |
+
Stores generated documentation in FAISS with sentence-transformers embeddings.
|
| 25 |
+
|
| 26 |
+
Args:
|
| 27 |
+
repo_url: The repository URL (used for index filename)
|
| 28 |
+
docs_list: List of dicts containing 'filename' and 'documentation'
|
| 29 |
+
"""
|
| 30 |
+
repo_slug = _sanitize_repo_url(repo_url)
|
| 31 |
+
print(f"[STORE_DOCS] Starting storage for repo: {repo_url}")
|
| 32 |
+
print(f"[STORE_DOCS] Repo slug: {repo_slug}")
|
| 33 |
+
|
| 34 |
+
# Extract all documentation text
|
| 35 |
+
doc_texts = []
|
| 36 |
+
doc_metadata = []
|
| 37 |
+
|
| 38 |
+
for doc_item in docs_list:
|
| 39 |
+
text = doc_item['documentation']
|
| 40 |
+
filename = doc_item['filename']
|
| 41 |
+
doc_texts.append(text)
|
| 42 |
+
doc_metadata.append({'filename': filename, 'repo_url': repo_url})
|
| 43 |
+
|
| 44 |
+
print(f"[STORE_DOCS] Extracted {len(doc_texts)} documents")
|
| 45 |
+
|
| 46 |
+
if not doc_texts:
|
| 47 |
+
print(f"[STORE_DOCS] No documents to store, returning early")
|
| 48 |
+
return
|
| 49 |
+
|
| 50 |
+
# Generate embeddings for all documents
|
| 51 |
+
print(f"[STORE_DOCS] Generating embeddings for {len(doc_texts)} documents...")
|
| 52 |
+
embeddings = model.encode(doc_texts, convert_to_numpy=True)
|
| 53 |
+
print(f"[STORE_DOCS] Embeddings shape: {embeddings.shape}")
|
| 54 |
+
|
| 55 |
+
# Create FAISS index
|
| 56 |
+
dimension = embeddings.shape[1]
|
| 57 |
+
index = faiss.IndexFlatL2(dimension)
|
| 58 |
+
index.add(embeddings.astype(np.float32))
|
| 59 |
+
print(f"[STORE_DOCS] FAISS index created with {index.ntotal} vectors")
|
| 60 |
+
|
| 61 |
+
# Save FAISS index
|
| 62 |
+
index_path = INDEXES_DIR / f'{repo_slug}.index'
|
| 63 |
+
faiss.write_index(index, str(index_path))
|
| 64 |
+
print(f"[STORE_DOCS] FAISS index saved to: {index_path}")
|
| 65 |
+
|
| 66 |
+
# Save text chunks and metadata
|
| 67 |
+
texts_path = INDEXES_DIR / f'{repo_slug}_texts.json'
|
| 68 |
+
with open(texts_path, 'w') as f:
|
| 69 |
+
json.dump({
|
| 70 |
+
'texts': doc_texts,
|
| 71 |
+
'metadata': doc_metadata,
|
| 72 |
+
'repo_url': repo_url
|
| 73 |
+
}, f)
|
| 74 |
+
print(f"[STORE_DOCS] Metadata saved to: {texts_path}")
|
| 75 |
+
print(f"[STORE_DOCS] Storage complete!")
|
| 76 |
+
|
| 77 |
+
|
| 78 |
+
def update_docs(repo_url: str, updated_docs_list: List[Dict[str, Any]]) -> None:
|
| 79 |
+
"""
|
| 80 |
+
Updates documentation for specific files in an existing FAISS index.
|
| 81 |
+
Loads existing index, updates entries for the specified files, and rebuilds.
|
| 82 |
+
|
| 83 |
+
Args:
|
| 84 |
+
repo_url: The repository URL
|
| 85 |
+
updated_docs_list: List of dicts with 'filename' and 'documentation' to update
|
| 86 |
+
"""
|
| 87 |
+
repo_slug = _sanitize_repo_url(repo_url)
|
| 88 |
+
print(f"[UPDATE_DOCS] Starting update for repo: {repo_url}")
|
| 89 |
+
print(f"[UPDATE_DOCS] Updating {len(updated_docs_list)} files")
|
| 90 |
+
|
| 91 |
+
index_path = INDEXES_DIR / f'{repo_slug}.index'
|
| 92 |
+
texts_path = INDEXES_DIR / f'{repo_slug}_texts.json'
|
| 93 |
+
|
| 94 |
+
# Load existing index and texts
|
| 95 |
+
if not index_path.exists() or not texts_path.exists():
|
| 96 |
+
print(f"[UPDATE_DOCS] No existing index found, creating new one")
|
| 97 |
+
# No existing index, create new one with these docs
|
| 98 |
+
store_docs(repo_url, updated_docs_list)
|
| 99 |
+
return
|
| 100 |
+
|
| 101 |
+
try:
|
| 102 |
+
# Load existing data
|
| 103 |
+
with open(texts_path, 'r') as f:
|
| 104 |
+
data = json.load(f)
|
| 105 |
+
|
| 106 |
+
existing_texts = data['texts']
|
| 107 |
+
existing_metadata = data['metadata']
|
| 108 |
+
print(f"[UPDATE_DOCS] Loaded {len(existing_texts)} existing documents")
|
| 109 |
+
|
| 110 |
+
# Get filenames being updated
|
| 111 |
+
updated_filenames = {doc['filename'] for doc in updated_docs_list}
|
| 112 |
+
|
| 113 |
+
# Remove entries for files being updated
|
| 114 |
+
filtered_texts = []
|
| 115 |
+
filtered_metadata = []
|
| 116 |
+
for text, meta in zip(existing_texts, existing_metadata):
|
| 117 |
+
if meta.get('filename') not in updated_filenames:
|
| 118 |
+
filtered_texts.append(text)
|
| 119 |
+
filtered_metadata.append(meta)
|
| 120 |
+
|
| 121 |
+
print(f"[UPDATE_DOCS] After filtering: {len(filtered_texts)} documents remain")
|
| 122 |
+
|
| 123 |
+
# Add new entries
|
| 124 |
+
for doc in updated_docs_list:
|
| 125 |
+
filtered_texts.append(doc['documentation'])
|
| 126 |
+
filtered_metadata.append({
|
| 127 |
+
'filename': doc['filename'],
|
| 128 |
+
'repo_url': repo_url
|
| 129 |
+
})
|
| 130 |
+
|
| 131 |
+
print(f"[UPDATE_DOCS] After adding updates: {len(filtered_texts)} documents total")
|
| 132 |
+
|
| 133 |
+
if not filtered_texts:
|
| 134 |
+
print(f"[UPDATE_DOCS] No documents remaining, returning early")
|
| 135 |
+
return
|
| 136 |
+
|
| 137 |
+
# Rebuild FAISS index
|
| 138 |
+
print(f"[UPDATE_DOCS] Rebuilding FAISS index...")
|
| 139 |
+
embeddings = model.encode(filtered_texts, convert_to_numpy=True)
|
| 140 |
+
dimension = embeddings.shape[1]
|
| 141 |
+
index = faiss.IndexFlatL2(dimension)
|
| 142 |
+
index.add(embeddings.astype(np.float32))
|
| 143 |
+
print(f"[UPDATE_DOCS] FAISS index rebuilt with {index.ntotal} vectors")
|
| 144 |
+
|
| 145 |
+
# Save updated index
|
| 146 |
+
faiss.write_index(index, str(index_path))
|
| 147 |
+
print(f"[UPDATE_DOCS] Updated FAISS index saved to: {index_path}")
|
| 148 |
+
|
| 149 |
+
# Save updated texts
|
| 150 |
+
with open(texts_path, 'w') as f:
|
| 151 |
+
json.dump({
|
| 152 |
+
'texts': filtered_texts,
|
| 153 |
+
'metadata': filtered_metadata,
|
| 154 |
+
'repo_url': repo_url
|
| 155 |
+
}, f)
|
| 156 |
+
print(f"[UPDATE_DOCS] Updated metadata saved to: {texts_path}")
|
| 157 |
+
print(f"[UPDATE_DOCS] Update complete!")
|
| 158 |
+
|
| 159 |
+
except Exception as e:
|
| 160 |
+
print(f"Error updating index for {repo_url}: {e}")
|
| 161 |
+
raise
|
| 162 |
+
|
| 163 |
+
|
| 164 |
+
def search_docs(repo_url: str, question: str, num_results: int = 3) -> List[Dict[str, Any]]:
|
| 165 |
+
"""
|
| 166 |
+
Searches stored documentation using semantic similarity.
|
| 167 |
+
|
| 168 |
+
Args:
|
| 169 |
+
repo_url: The repository URL
|
| 170 |
+
question: Natural language question to search for
|
| 171 |
+
num_results: Number of top results to return (default: 3)
|
| 172 |
+
|
| 173 |
+
Returns:
|
| 174 |
+
List of dicts containing 'document', 'filename', and 'distance'
|
| 175 |
+
"""
|
| 176 |
+
repo_slug = _sanitize_repo_url(repo_url)
|
| 177 |
+
print(f"[SEARCH_DOCS] Searching for: '{question}' in repo: {repo_url}")
|
| 178 |
+
|
| 179 |
+
# Check if index exists
|
| 180 |
+
index_path = INDEXES_DIR / f'{repo_slug}.index'
|
| 181 |
+
texts_path = INDEXES_DIR / f'{repo_slug}_texts.json'
|
| 182 |
+
|
| 183 |
+
if not index_path.exists() or not texts_path.exists():
|
| 184 |
+
print(f"[SEARCH_DOCS] ERROR: No index found for repo. Please generate documentation first.")
|
| 185 |
+
print(f"[SEARCH_DOCS] Expected paths: {index_path} and {texts_path}")
|
| 186 |
+
return []
|
| 187 |
+
|
| 188 |
+
try:
|
| 189 |
+
# Load FAISS index
|
| 190 |
+
print(f"[SEARCH_DOCS] Loading FAISS index from: {index_path}")
|
| 191 |
+
index = faiss.read_index(str(index_path))
|
| 192 |
+
print(f"[SEARCH_DOCS] FAISS index loaded with {index.ntotal} vectors")
|
| 193 |
+
|
| 194 |
+
# Load text chunks
|
| 195 |
+
with open(texts_path, 'r') as f:
|
| 196 |
+
data = json.load(f)
|
| 197 |
+
|
| 198 |
+
texts = data['texts']
|
| 199 |
+
metadata = data['metadata']
|
| 200 |
+
print(f"[SEARCH_DOCS] Loaded {len(texts)} text documents and metadata")
|
| 201 |
+
except Exception as e:
|
| 202 |
+
print(f"[SEARCH_DOCS] Error loading index for {repo_url}: {e}")
|
| 203 |
+
return []
|
| 204 |
+
|
| 205 |
+
# Embed the question
|
| 206 |
+
print(f"[SEARCH_DOCS] Encoding question embedding...")
|
| 207 |
+
question_embedding = model.encode([question], convert_to_numpy=True)
|
| 208 |
+
|
| 209 |
+
# Search the index
|
| 210 |
+
print(f"[SEARCH_DOCS] Searching for top {min(num_results, len(texts))} results...")
|
| 211 |
+
distances, indices = index.search(question_embedding.astype(np.float32), min(num_results, len(texts)))
|
| 212 |
+
|
| 213 |
+
# Format results
|
| 214 |
+
formatted_results = []
|
| 215 |
+
for idx, distance in zip(indices[0], distances[0]):
|
| 216 |
+
if idx == -1: # No valid result
|
| 217 |
+
continue
|
| 218 |
+
formatted_results.append({
|
| 219 |
+
'document': texts[idx],
|
| 220 |
+
'filename': metadata[idx].get('filename', 'unknown'),
|
| 221 |
+
'distance': float(distance)
|
| 222 |
+
})
|
| 223 |
+
|
| 224 |
+
print(f"[SEARCH_DOCS] Found {len(formatted_results)} results")
|
| 225 |
+
return formatted_results
|
docs/.gitkeep
ADDED
|
File without changes
|
frontend/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
module.exports = {
|
| 2 |
+
root: true,
|
| 3 |
+
env: { browser: true, es2020: true },
|
| 4 |
+
extends: [
|
| 5 |
+
'eslint:recommended',
|
| 6 |
+
'plugin:react/recommended',
|
| 7 |
+
'plugin:react/jsx-runtime',
|
| 8 |
+
'plugin:react-hooks/recommended',
|
| 9 |
+
],
|
| 10 |
+
ignorePatterns: ['dist', '.eslintrc.cjs'],
|
| 11 |
+
parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
|
| 12 |
+
settings: { react: { version: '18.2' } },
|
| 13 |
+
plugins: ['react-refresh'],
|
| 14 |
+
rules: {
|
| 15 |
+
'react-refresh/only-export-components': [
|
| 16 |
+
'warn',
|
| 17 |
+
{ allowConstantExport: true },
|
| 18 |
+
],
|
| 19 |
+
},
|
| 20 |
+
}
|
frontend/.gitignore
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Logs
|
| 2 |
+
logs
|
| 3 |
+
*.log
|
| 4 |
+
npm-debug.log*
|
| 5 |
+
yarn-debug.log*
|
| 6 |
+
yarn-error.log*
|
| 7 |
+
pnpm-debug.log*
|
| 8 |
+
lerna-debug.log*
|
| 9 |
+
|
| 10 |
+
node_modules
|
| 11 |
+
dist
|
| 12 |
+
dist-ssr
|
| 13 |
+
*.local
|
| 14 |
+
|
| 15 |
+
# Editor directories and files
|
| 16 |
+
.vscode/*
|
| 17 |
+
!.vscode/extensions.json
|
| 18 |
+
.idea
|
| 19 |
+
.DS_Store
|
| 20 |
+
*.suo
|
| 21 |
+
*.ntvs*
|
| 22 |
+
*.njsproj
|
| 23 |
+
*.sln
|
| 24 |
+
*.sw?
|
frontend/README.md
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# DocWizard 🧙♂️
|
| 2 |
+
|
| 3 |
+
> AI-powered code documentation and search agent — paste a GitHub URL, get professional docs in seconds.
|
| 4 |
+
|
| 5 |
+
---
|
| 6 |
+
|
| 7 |
+
## What It Does
|
| 8 |
+
|
| 9 |
+
DocWizard takes any public GitHub repository, reads every Python file, and automatically generates clean human-readable documentation using AI. It also lets developers search the codebase in plain English — no more digging through files manually.
|
| 10 |
+
|
| 11 |
+
**Core features:**
|
| 12 |
+
- Generates documentation for every file in a repo with one click
|
| 13 |
+
- Answers natural language questions about the codebase
|
| 14 |
+
- Semantic search powered by FAISS vector embeddings
|
| 15 |
+
- Download individual file docs or all docs as a ZIP
|
| 16 |
+
- Auto-generates README.md and .gitignore for any project
|
| 17 |
+
|
| 18 |
+
---
|
| 19 |
+
|
| 20 |
+
## Tech Stack
|
| 21 |
+
|
| 22 |
+
| Layer | Technology |
|
| 23 |
+
|-------|------------|
|
| 24 |
+
| Backend | Python 3.10+, FastAPI, Uvicorn |
|
| 25 |
+
| LLM | OpenRouter API (qwen/qwen3-coder:free) |
|
| 26 |
+
| Embeddings | sentence-transformers (all-MiniLM-L6-v2) |
|
| 27 |
+
| Vector Search | FAISS (faiss-cpu) |
|
| 28 |
+
| Code Parsing | Python AST (built-in) |
|
| 29 |
+
| GitHub Access | GitHub REST API v3 |
|
| 30 |
+
| Frontend | React 18, Vite, Tailwind CSS |
|
| 31 |
+
| File Handling | JSZip (client-side ZIP generation) |
|
| 32 |
+
|
| 33 |
+
**Total cost to run: $0** — all free tier services, no credit card required.
|
| 34 |
+
|
| 35 |
+
---
|
| 36 |
+
|
| 37 |
+
## Project Structure
|
| 38 |
+
|
| 39 |
+
```
|
| 40 |
+
DocWizard/
|
| 41 |
+
├── backend/
|
| 42 |
+
│ ├── main.py # FastAPI app, all API endpoints
|
| 43 |
+
│ ├── doc_generator.py # OpenRouter API calls, doc generation logic
|
| 44 |
+
│ ├── parser.py # AST-based Python code parser
|
| 45 |
+
│ ├── vector_store.py # FAISS index creation and semantic search
|
| 46 |
+
│ ├── github_client.py # GitHub API, repo file fetching
|
| 47 |
+
│ └── requirements.txt # Python dependencies
|
| 48 |
+
├── frontend/
|
| 49 |
+
│ └── src/
|
| 50 |
+
│ ├── App.jsx # Main React component, full UI
|
| 51 |
+
│ └── components/ # UI sub-components
|
| 52 |
+
├── faiss_indexes/ # Auto-generated, gitignored
|
| 53 |
+
├── .env # Your API keys, gitignored
|
| 54 |
+
├── .env.example # Template for environment variables
|
| 55 |
+
├── .gitignore
|
| 56 |
+
└── README.md
|
| 57 |
+
```
|
| 58 |
+
|
| 59 |
+
---
|
| 60 |
+
|
| 61 |
+
## Prerequisites
|
| 62 |
+
|
| 63 |
+
Make sure these are installed on your machine before starting:
|
| 64 |
+
|
| 65 |
+
- [Python 3.10+](https://www.python.org/downloads/)
|
| 66 |
+
- [Node.js 18+](https://nodejs.org/)
|
| 67 |
+
- [Git](https://git-scm.com/)
|
| 68 |
+
|
| 69 |
+
---
|
| 70 |
+
|
| 71 |
+
## Installation
|
| 72 |
+
|
| 73 |
+
### 1. Clone the repository
|
| 74 |
+
|
| 75 |
+
```bash
|
| 76 |
+
git clone https://github.com/yourusername/docwizard.git
|
| 77 |
+
cd docwizard
|
| 78 |
+
```
|
| 79 |
+
|
| 80 |
+
### 2. Get your API keys
|
| 81 |
+
|
| 82 |
+
**OpenRouter API key** (free, no credit card):
|
| 83 |
+
1. Go to [openrouter.ai](https://openrouter.ai)
|
| 84 |
+
2. Sign up → Profile → Keys → Create Key
|
| 85 |
+
3. Copy the key (starts with `sk-or-...`)
|
| 86 |
+
|
| 87 |
+
**GitHub Personal Access Token** (free):
|
| 88 |
+
1. Go to GitHub → Settings → Developer settings
|
| 89 |
+
2. Personal access tokens → Tokens (classic) → Generate new token
|
| 90 |
+
3. Check only the `repo` scope → Generate → Copy the token (starts with `ghp_...`)
|
| 91 |
+
|
| 92 |
+
### 3. Set up environment variables
|
| 93 |
+
|
| 94 |
+
```bash
|
| 95 |
+
cp .env.example .env
|
| 96 |
+
```
|
| 97 |
+
|
| 98 |
+
Open `.env` and fill in your keys:
|
| 99 |
+
|
| 100 |
+
```env
|
| 101 |
+
OPENROUTER_API_KEY=sk-or-your-key-here
|
| 102 |
+
GITHUB_TOKEN=ghp_your-token-here
|
| 103 |
+
LLM_MODEL=qwen/qwen3-coder:free
|
| 104 |
+
```
|
| 105 |
+
|
| 106 |
+
### 4. Install backend dependencies
|
| 107 |
+
|
| 108 |
+
```bash
|
| 109 |
+
cd backend
|
| 110 |
+
pip install "numpy<2"
|
| 111 |
+
pip install -r requirements.txt
|
| 112 |
+
```
|
| 113 |
+
|
| 114 |
+
> **Windows note:** If `faiss-cpu` fails to install, run `pip install faiss-cpu --prefer-binary` instead.
|
| 115 |
+
|
| 116 |
+
### 5. Install frontend dependencies
|
| 117 |
+
|
| 118 |
+
```bash
|
| 119 |
+
cd ../frontend
|
| 120 |
+
npm install
|
| 121 |
+
```
|
| 122 |
+
|
| 123 |
+
---
|
| 124 |
+
|
| 125 |
+
## Running the App
|
| 126 |
+
|
| 127 |
+
You need two terminals open simultaneously.
|
| 128 |
+
|
| 129 |
+
**Terminal 1 — Backend:**
|
| 130 |
+
```bash
|
| 131 |
+
cd backend
|
| 132 |
+
uvicorn main:app --reload
|
| 133 |
+
```
|
| 134 |
+
Backend runs at `http://127.0.0.1:8000`
|
| 135 |
+
|
| 136 |
+
**Terminal 2 — Frontend:**
|
| 137 |
+
```bash
|
| 138 |
+
cd frontend
|
| 139 |
+
npm run dev
|
| 140 |
+
```
|
| 141 |
+
Frontend runs at `http://localhost:5173`
|
| 142 |
+
|
| 143 |
+
Open `http://localhost:5173` in your browser.
|
| 144 |
+
|
| 145 |
+
---
|
| 146 |
+
|
| 147 |
+
## How to Use
|
| 148 |
+
|
| 149 |
+
1. Paste any public GitHub repository URL into the input field
|
| 150 |
+
- Example: `https://github.com/tiangolo/fastapi`
|
| 151 |
+
2. Click **Generate Docs** and wait 10–30 seconds
|
| 152 |
+
3. Documentation appears for every Python file in the repo
|
| 153 |
+
4. Click any file card to expand and read its docs
|
| 154 |
+
5. Use the **Copy** or **Download .md** button on each card
|
| 155 |
+
6. Click **Download All as ZIP** to get everything at once
|
| 156 |
+
7. Type a question in the search bar to query the codebase
|
| 157 |
+
- Example: *"How do I handle authentication?"*
|
| 158 |
+
8. Click **Generate README** to create a README for that repo
|
| 159 |
+
9. Click **Generate .gitignore** for a project-specific gitignore
|
| 160 |
+
|
| 161 |
+
---
|
| 162 |
+
|
| 163 |
+
## API Endpoints
|
| 164 |
+
|
| 165 |
+
| Method | Endpoint | Description |
|
| 166 |
+
|--------|----------|-------------|
|
| 167 |
+
| POST | `/generate-docs` | Generate docs for a GitHub repo |
|
| 168 |
+
| POST | `/ask` | Ask a natural language question |
|
| 169 |
+
| POST | `/generate-readme` | Generate a README.md for the repo |
|
| 170 |
+
| POST | `/generate-gitignore` | Generate a .gitignore for the repo |
|
| 171 |
+
|
| 172 |
+
**Example request:**
|
| 173 |
+
```bash
|
| 174 |
+
curl -X POST http://localhost:8000/generate-docs \
|
| 175 |
+
-H "Content-Type: application/json" \
|
| 176 |
+
-d '{"repo_url": "https://github.com/tiangolo/fastapi"}'
|
| 177 |
+
```
|
| 178 |
+
|
| 179 |
+
---
|
| 180 |
+
|
| 181 |
+
## Environment Variables
|
| 182 |
+
|
| 183 |
+
| Variable | Description | Required |
|
| 184 |
+
|----------|-------------|----------|
|
| 185 |
+
| `OPENROUTER_API_KEY` | Your OpenRouter API key | Yes |
|
| 186 |
+
| `GITHUB_TOKEN` | GitHub personal access token | Yes |
|
| 187 |
+
| `LLM_MODEL` | Model to use (default: `qwen/qwen3-coder:free`) | No |
|
| 188 |
+
|
| 189 |
+
---
|
| 190 |
+
|
| 191 |
+
## Known Limitations
|
| 192 |
+
|
| 193 |
+
- **Free tier rate limits:** OpenRouter free tier allows 50 requests/day and 20 requests/minute. DocWizard batches all files into one request to stay within this limit.
|
| 194 |
+
- **Python only:** The AST parser currently supports Python files only. JavaScript and other languages are planned for a future update.
|
| 195 |
+
- **Public repos only:** Private repositories require additional GitHub token scopes.
|
| 196 |
+
- **Context window:** Very large repos (100+ files) may exceed the model's context window. DocWizard will document as many files as fit.
|
| 197 |
+
|
| 198 |
+
---
|
| 199 |
+
|
| 200 |
+
## Troubleshooting
|
| 201 |
+
|
| 202 |
+
**`uvicorn main:app --reload` gives NumPy error:**
|
| 203 |
+
```bash
|
| 204 |
+
pip install "numpy<2" --force-reinstall
|
| 205 |
+
```
|
| 206 |
+
|
| 207 |
+
**Search returns 0 results:**
|
| 208 |
+
Generate docs first before searching — the search index is built during doc generation.
|
| 209 |
+
|
| 210 |
+
**Rate limit error but OpenRouter shows 0 requests:**
|
| 211 |
+
Your `.env` file is not being loaded. Make sure `load_dotenv()` is called at the top of `doc_generator.py` and your `.env` file is in the `backend/` folder.
|
| 212 |
+
|
| 213 |
+
**`npm run dev` fails:**
|
| 214 |
+
Make sure you ran `npm install` inside the `frontend/` folder first.
|
| 215 |
+
|
| 216 |
+
---
|
| 217 |
+
|
| 218 |
+
## Roadmap
|
| 219 |
+
|
| 220 |
+
- [ ] JavaScript / TypeScript support
|
| 221 |
+
- [ ] Private repository support
|
| 222 |
+
- [ ] GitHub Action for auto-updating docs on push
|
| 223 |
+
- [ ] Change detection — only re-document modified files
|
| 224 |
+
- [ ] Architecture diagram generation
|
| 225 |
+
- [ ] Multi-language support (Java, Go, Rust)
|
| 226 |
+
|
| 227 |
+
---
|
| 228 |
+
|
| 229 |
+
## License
|
| 230 |
+
|
| 231 |
+
MIT License — free to use, modify, and distribute.
|
| 232 |
+
|
| 233 |
+
---
|
| 234 |
+
|
| 235 |
+
## Acknowledgements
|
| 236 |
+
|
| 237 |
+
- [OpenRouter](https://openrouter.ai) for free LLM access
|
| 238 |
+
- [FAISS](https://github.com/facebookresearch/faiss) by Meta for vector search
|
| 239 |
+
- [sentence-transformers](https://www.sbert.net/) for local embeddings
|
| 240 |
+
- [FastAPI](https://fastapi.tiangolo.com/) for the backend framework
|
frontend/index.html
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!doctype html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8" />
|
| 5 |
+
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
| 6 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
| 7 |
+
<title>Vite + React</title>
|
| 8 |
+
</head>
|
| 9 |
+
<body>
|
| 10 |
+
<div id="root"></div>
|
| 11 |
+
<script type="module" src="/src/main.jsx"></script>
|
| 12 |
+
</body>
|
| 13 |
+
</html>
|
frontend/package-lock.json
ADDED
|
The diff for this file is too large to render.
See raw diff
|
|
|
frontend/package.json
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{
|
| 2 |
+
"name": "frontend",
|
| 3 |
+
"private": true,
|
| 4 |
+
"version": "0.0.0",
|
| 5 |
+
"type": "module",
|
| 6 |
+
"scripts": {
|
| 7 |
+
"dev": "vite",
|
| 8 |
+
"build": "vite build",
|
| 9 |
+
"lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
|
| 10 |
+
"preview": "vite preview"
|
| 11 |
+
},
|
| 12 |
+
"dependencies": {
|
| 13 |
+
"@tailwindcss/typography": "^0.5.19",
|
| 14 |
+
"axios": "^1.14.0",
|
| 15 |
+
"marked": "^17.0.6",
|
| 16 |
+
"react": "^18.2.0",
|
| 17 |
+
"react-dom": "^18.2.0"
|
| 18 |
+
},
|
| 19 |
+
"devDependencies": {
|
| 20 |
+
"@types/react": "^18.2.15",
|
| 21 |
+
"@types/react-dom": "^18.2.7",
|
| 22 |
+
"@vitejs/plugin-react": "^4.0.3",
|
| 23 |
+
"autoprefixer": "^10.4.27",
|
| 24 |
+
"eslint": "^8.45.0",
|
| 25 |
+
"eslint-plugin-react": "^7.32.2",
|
| 26 |
+
"eslint-plugin-react-hooks": "^4.6.0",
|
| 27 |
+
"eslint-plugin-react-refresh": "^0.4.3",
|
| 28 |
+
"postcss": "^8.5.8",
|
| 29 |
+
"tailwindcss": "^3.4.19",
|
| 30 |
+
"vite": "^4.4.5"
|
| 31 |
+
}
|
| 32 |
+
}
|
frontend/postcss.config.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
export default {
|
| 2 |
+
plugins: {
|
| 3 |
+
tailwindcss: {},
|
| 4 |
+
autoprefixer: {},
|
| 5 |
+
},
|
| 6 |
+
}
|
frontend/public/vite.svg
ADDED
|
|
frontend/src/App.css
ADDED
|
File without changes
|
frontend/src/App.jsx
ADDED
|
@@ -0,0 +1,737 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { useState, useEffect } from 'react';
|
| 2 |
+
import { marked } from 'marked';
|
| 3 |
+
|
| 4 |
+
export default function App() {
|
| 5 |
+
// State management
|
| 6 |
+
const [repoUrl, setRepoUrl] = useState('');
|
| 7 |
+
const [generatedRepoUrl, setGeneratedRepoUrl] = useState('');
|
| 8 |
+
const [docs, setDocs] = useState(null);
|
| 9 |
+
const [loading, setLoading] = useState(false);
|
| 10 |
+
const [error, setError] = useState(null);
|
| 11 |
+
const [progress, setProgress] = useState(null);
|
| 12 |
+
const [statusMessage, setStatusMessage] = useState('');
|
| 13 |
+
const [expandedFiles, setExpandedFiles] = useState({}); // Track which files are expanded
|
| 14 |
+
const [searchQuery, setSearchQuery] = useState('');
|
| 15 |
+
const [searchResults, setSearchResults] = useState(null);
|
| 16 |
+
const [searchLoading, setSearchLoading] = useState(false);
|
| 17 |
+
const [searchError, setSearchError] = useState(null);
|
| 18 |
+
const [jsZipLoaded, setJsZipLoaded] = useState(false);
|
| 19 |
+
const [readme, setReadme] = useState(null);
|
| 20 |
+
const [readmeLoading, setReadmeLoading] = useState(false);
|
| 21 |
+
const [readmeError, setReadmeError] = useState(null);
|
| 22 |
+
const [gitignore, setGitignore] = useState(null);
|
| 23 |
+
const [gitignoreLoading, setGitignoreLoading] = useState(false);
|
| 24 |
+
const [gitignoreError, setGitignoreError] = useState(null);
|
| 25 |
+
|
| 26 |
+
const API_BASE = 'http://localhost:8000';
|
| 27 |
+
|
| 28 |
+
// Load JSZip library on mount
|
| 29 |
+
useEffect(() => {
|
| 30 |
+
const script = document.createElement('script');
|
| 31 |
+
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/jszip/3.10.1/jszip.min.js';
|
| 32 |
+
script.onload = () => setJsZipLoaded(true);
|
| 33 |
+
document.head.appendChild(script);
|
| 34 |
+
}, []);
|
| 35 |
+
|
| 36 |
+
// Utility: Copy to clipboard
|
| 37 |
+
const copyToClipboard = (text, filename) => {
|
| 38 |
+
navigator.clipboard.writeText(text).then(() => {
|
| 39 |
+
alert(`Copied ${filename} to clipboard!`);
|
| 40 |
+
}).catch(() => {
|
| 41 |
+
alert('Failed to copy to clipboard');
|
| 42 |
+
});
|
| 43 |
+
};
|
| 44 |
+
|
| 45 |
+
// Utility: Download single markdown file
|
| 46 |
+
const downloadFile = (content, filename) => {
|
| 47 |
+
const blob = new Blob([content], { type: 'text/markdown' });
|
| 48 |
+
const url = URL.createObjectURL(blob);
|
| 49 |
+
const a = document.createElement('a');
|
| 50 |
+
a.href = url;
|
| 51 |
+
a.download = `${filename}.md`;
|
| 52 |
+
document.body.appendChild(a);
|
| 53 |
+
a.click();
|
| 54 |
+
document.body.removeChild(a);
|
| 55 |
+
URL.revokeObjectURL(url);
|
| 56 |
+
};
|
| 57 |
+
|
| 58 |
+
// Utility: Download all docs as ZIP
|
| 59 |
+
const downloadAllAsZip = async () => {
|
| 60 |
+
if (!docs || docs.length === 0 || !window.JSZip) {
|
| 61 |
+
alert('JSZip library not loaded or no docs available');
|
| 62 |
+
return;
|
| 63 |
+
}
|
| 64 |
+
|
| 65 |
+
try {
|
| 66 |
+
const zip = new window.JSZip();
|
| 67 |
+
docs.forEach(file => {
|
| 68 |
+
zip.file(`${file.filename}.md`, file.documentation);
|
| 69 |
+
});
|
| 70 |
+
|
| 71 |
+
const blob = await zip.generateAsync({ type: 'blob' });
|
| 72 |
+
const url = URL.createObjectURL(blob);
|
| 73 |
+
const a = document.createElement('a');
|
| 74 |
+
a.href = url;
|
| 75 |
+
a.download = `docwizard-${new Date().getTime()}.zip`;
|
| 76 |
+
document.body.appendChild(a);
|
| 77 |
+
a.click();
|
| 78 |
+
document.body.removeChild(a);
|
| 79 |
+
URL.revokeObjectURL(url);
|
| 80 |
+
} catch (err) {
|
| 81 |
+
console.error('Failed to download ZIP:', err);
|
| 82 |
+
alert('Failed to create ZIP file');
|
| 83 |
+
}
|
| 84 |
+
};
|
| 85 |
+
|
| 86 |
+
// Toggle accordion
|
| 87 |
+
const toggleFile = (filename) => {
|
| 88 |
+
setExpandedFiles(prev => ({
|
| 89 |
+
...prev,
|
| 90 |
+
[filename]: !prev[filename]
|
| 91 |
+
}));
|
| 92 |
+
};
|
| 93 |
+
|
| 94 |
+
const handleGenerateDocs = async () => {
|
| 95 |
+
if (!repoUrl.trim()) {
|
| 96 |
+
setError('Please enter a GitHub repository URL');
|
| 97 |
+
return;
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
setLoading(true);
|
| 101 |
+
setError(null);
|
| 102 |
+
setDocs(null);
|
| 103 |
+
setProgress(null);
|
| 104 |
+
setStatusMessage('');
|
| 105 |
+
setExpandedFiles({});
|
| 106 |
+
|
| 107 |
+
try {
|
| 108 |
+
const response = await fetch(`${API_BASE}/generate-docs`, {
|
| 109 |
+
method: 'POST',
|
| 110 |
+
headers: {
|
| 111 |
+
'Content-Type': 'application/json',
|
| 112 |
+
},
|
| 113 |
+
body: JSON.stringify({ repo_url: repoUrl }),
|
| 114 |
+
});
|
| 115 |
+
|
| 116 |
+
if (!response.ok) {
|
| 117 |
+
throw new Error('Failed to generate documentation');
|
| 118 |
+
}
|
| 119 |
+
|
| 120 |
+
const reader = response.body.getReader();
|
| 121 |
+
const decoder = new TextDecoder();
|
| 122 |
+
const processedFiles = [];
|
| 123 |
+
let buffer = '';
|
| 124 |
+
|
| 125 |
+
while (true) {
|
| 126 |
+
const { done, value } = await reader.read();
|
| 127 |
+
if (done) break;
|
| 128 |
+
|
| 129 |
+
buffer += decoder.decode(value, { stream: true });
|
| 130 |
+
const lines = buffer.split('\n');
|
| 131 |
+
|
| 132 |
+
for (let i = 0; i < lines.length - 1; i++) {
|
| 133 |
+
const line = lines[i].trim();
|
| 134 |
+
if (!line) continue;
|
| 135 |
+
|
| 136 |
+
try {
|
| 137 |
+
const data = JSON.parse(line);
|
| 138 |
+
|
| 139 |
+
if (data.status === 'file_processed') {
|
| 140 |
+
processedFiles.push(data.file);
|
| 141 |
+
setProgress({
|
| 142 |
+
current: data.current,
|
| 143 |
+
total: data.total,
|
| 144 |
+
});
|
| 145 |
+
setStatusMessage(`Processed ${data.current}/${data.total} files`);
|
| 146 |
+
} else if (data.status === 'rate_limit') {
|
| 147 |
+
setStatusMessage(data.message);
|
| 148 |
+
} else if (data.status === 'complete') {
|
| 149 |
+
setProgress(null);
|
| 150 |
+
setStatusMessage('');
|
| 151 |
+
setDocs(processedFiles);
|
| 152 |
+
setGeneratedRepoUrl(repoUrl);
|
| 153 |
+
} else if (data.status === 'error') {
|
| 154 |
+
throw new Error(data.message);
|
| 155 |
+
}
|
| 156 |
+
} catch (parseError) {
|
| 157 |
+
console.error('Failed to parse line:', line, parseError);
|
| 158 |
+
}
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
buffer = lines[lines.length - 1];
|
| 162 |
+
}
|
| 163 |
+
|
| 164 |
+
if (buffer.trim()) {
|
| 165 |
+
try {
|
| 166 |
+
const data = JSON.parse(buffer);
|
| 167 |
+
if (data.status === 'error') {
|
| 168 |
+
throw new Error(data.message);
|
| 169 |
+
}
|
| 170 |
+
} catch (parseError) {
|
| 171 |
+
console.error('Failed to parse final buffer:', buffer, parseError);
|
| 172 |
+
}
|
| 173 |
+
}
|
| 174 |
+
} catch (err) {
|
| 175 |
+
setError(err.message || 'An error occurred');
|
| 176 |
+
setDocs(null);
|
| 177 |
+
setProgress(null);
|
| 178 |
+
} finally {
|
| 179 |
+
setLoading(false);
|
| 180 |
+
setStatusMessage('');
|
| 181 |
+
}
|
| 182 |
+
};
|
| 183 |
+
|
| 184 |
+
const handleSearch = async () => {
|
| 185 |
+
// Validate that docs have been generated
|
| 186 |
+
if (!generatedRepoUrl) {
|
| 187 |
+
setSearchError('Please generate documentation for a repository first');
|
| 188 |
+
return;
|
| 189 |
+
}
|
| 190 |
+
|
| 191 |
+
if (!searchQuery.trim()) {
|
| 192 |
+
setSearchError('Please enter a search question');
|
| 193 |
+
return;
|
| 194 |
+
}
|
| 195 |
+
|
| 196 |
+
setSearchLoading(true);
|
| 197 |
+
setSearchError(null);
|
| 198 |
+
setSearchResults(null);
|
| 199 |
+
|
| 200 |
+
try {
|
| 201 |
+
const response = await fetch(`${API_BASE}/ask`, {
|
| 202 |
+
method: 'POST',
|
| 203 |
+
headers: {
|
| 204 |
+
'Content-Type': 'application/json',
|
| 205 |
+
},
|
| 206 |
+
body: JSON.stringify({
|
| 207 |
+
repo_url: generatedRepoUrl,
|
| 208 |
+
question: searchQuery,
|
| 209 |
+
}),
|
| 210 |
+
});
|
| 211 |
+
|
| 212 |
+
if (!response.ok) {
|
| 213 |
+
throw new Error('Failed to search documentation');
|
| 214 |
+
}
|
| 215 |
+
|
| 216 |
+
const data = await response.json();
|
| 217 |
+
if (data.status === 'success') {
|
| 218 |
+
setSearchResults(data.results);
|
| 219 |
+
} else {
|
| 220 |
+
setSearchError(data.message || 'Error searching documentation');
|
| 221 |
+
}
|
| 222 |
+
} catch (err) {
|
| 223 |
+
setSearchError(err.message || 'An error occurred');
|
| 224 |
+
} finally {
|
| 225 |
+
setSearchLoading(false);
|
| 226 |
+
}
|
| 227 |
+
};
|
| 228 |
+
|
| 229 |
+
const handleGenerateReadme = async () => {
|
| 230 |
+
if (!generatedRepoUrl) {
|
| 231 |
+
setReadmeError('Please generate documentation first');
|
| 232 |
+
return;
|
| 233 |
+
}
|
| 234 |
+
|
| 235 |
+
setReadmeLoading(true);
|
| 236 |
+
setReadmeError(null);
|
| 237 |
+
setReadme(null);
|
| 238 |
+
|
| 239 |
+
try {
|
| 240 |
+
const response = await fetch(`${API_BASE}/generate-readme`, {
|
| 241 |
+
method: 'POST',
|
| 242 |
+
headers: {
|
| 243 |
+
'Content-Type': 'application/json',
|
| 244 |
+
},
|
| 245 |
+
body: JSON.stringify({
|
| 246 |
+
repo_url: generatedRepoUrl,
|
| 247 |
+
}),
|
| 248 |
+
});
|
| 249 |
+
|
| 250 |
+
if (!response.ok) {
|
| 251 |
+
throw new Error('Failed to generate README');
|
| 252 |
+
}
|
| 253 |
+
|
| 254 |
+
const data = await response.json();
|
| 255 |
+
if (data.status === 'success') {
|
| 256 |
+
setReadme(data.readme);
|
| 257 |
+
} else {
|
| 258 |
+
setReadmeError(data.message || 'Error generating README');
|
| 259 |
+
}
|
| 260 |
+
} catch (err) {
|
| 261 |
+
setReadmeError(err.message || 'An error occurred');
|
| 262 |
+
} finally {
|
| 263 |
+
setReadmeLoading(false);
|
| 264 |
+
}
|
| 265 |
+
};
|
| 266 |
+
|
| 267 |
+
const handleGenerateGitignore = async () => {
|
| 268 |
+
if (!generatedRepoUrl) {
|
| 269 |
+
setGitignoreError('Please generate documentation first');
|
| 270 |
+
return;
|
| 271 |
+
}
|
| 272 |
+
|
| 273 |
+
setGitignoreLoading(true);
|
| 274 |
+
setGitignoreError(null);
|
| 275 |
+
setGitignore(null);
|
| 276 |
+
|
| 277 |
+
try {
|
| 278 |
+
const response = await fetch(`${API_BASE}/generate-gitignore`, {
|
| 279 |
+
method: 'POST',
|
| 280 |
+
headers: {
|
| 281 |
+
'Content-Type': 'application/json',
|
| 282 |
+
},
|
| 283 |
+
body: JSON.stringify({
|
| 284 |
+
repo_url: generatedRepoUrl,
|
| 285 |
+
}),
|
| 286 |
+
});
|
| 287 |
+
|
| 288 |
+
if (!response.ok) {
|
| 289 |
+
throw new Error('Failed to generate .gitignore');
|
| 290 |
+
}
|
| 291 |
+
|
| 292 |
+
const data = await response.json();
|
| 293 |
+
if (data.status === 'success') {
|
| 294 |
+
setGitignore(data.gitignore);
|
| 295 |
+
} else {
|
| 296 |
+
setGitignoreError(data.message || 'Error generating .gitignore');
|
| 297 |
+
}
|
| 298 |
+
} catch (err) {
|
| 299 |
+
setGitignoreError(err.message || 'An error occurred');
|
| 300 |
+
} finally {
|
| 301 |
+
setGitignoreLoading(false);
|
| 302 |
+
}
|
| 303 |
+
};
|
| 304 |
+
|
| 305 |
+
return (
|
| 306 |
+
<div className="min-h-screen bg-white">
|
| 307 |
+
{/* Header */}
|
| 308 |
+
<header className="bg-white border-b border-gray-200 sticky top-0 z-50 shadow-sm">
|
| 309 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
| 310 |
+
<div>
|
| 311 |
+
<h1 className="text-3xl font-bold bg-gradient-to-r from-teal-600 to-teal-500 bg-clip-text text-transparent">
|
| 312 |
+
DocWizard
|
| 313 |
+
</h1>
|
| 314 |
+
<p className="text-gray-600 text-sm mt-1">AI-powered code documentation</p>
|
| 315 |
+
</div>
|
| 316 |
+
</div>
|
| 317 |
+
</header>
|
| 318 |
+
|
| 319 |
+
{/* Main Content */}
|
| 320 |
+
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
|
| 321 |
+
{/* Input Section */}
|
| 322 |
+
<div className="mb-12">
|
| 323 |
+
<div className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 p-8 border border-gray-100">
|
| 324 |
+
<h2 className="text-xl font-semibold text-gray-900 mb-6">Generate Documentation</h2>
|
| 325 |
+
|
| 326 |
+
<div className="space-y-4">
|
| 327 |
+
<div>
|
| 328 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 329 |
+
GitHub Repository URL
|
| 330 |
+
</label>
|
| 331 |
+
<input
|
| 332 |
+
type="text"
|
| 333 |
+
value={repoUrl}
|
| 334 |
+
onChange={(e) => setRepoUrl(e.target.value)}
|
| 335 |
+
onKeyPress={(e) => e.key === 'Enter' && handleGenerateDocs()}
|
| 336 |
+
placeholder="https://github.com/user/repo"
|
| 337 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none transition text-gray-900"
|
| 338 |
+
/>
|
| 339 |
+
</div>
|
| 340 |
+
|
| 341 |
+
<button
|
| 342 |
+
onClick={handleGenerateDocs}
|
| 343 |
+
disabled={loading}
|
| 344 |
+
className="w-full bg-gradient-to-r from-teal-600 to-teal-500 hover:from-teal-700 hover:to-teal-600 disabled:from-gray-400 disabled:to-gray-400 text-white font-semibold py-3 px-4 rounded-lg transition duration-200 flex items-center justify-center gap-2"
|
| 345 |
+
>
|
| 346 |
+
{loading ? (
|
| 347 |
+
<>
|
| 348 |
+
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 349 |
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 350 |
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 351 |
+
</svg>
|
| 352 |
+
Generating Docs...
|
| 353 |
+
</>
|
| 354 |
+
) : (
|
| 355 |
+
<>
|
| 356 |
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 357 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
| 358 |
+
</svg>
|
| 359 |
+
Generate Docs
|
| 360 |
+
</>
|
| 361 |
+
)}
|
| 362 |
+
</button>
|
| 363 |
+
|
| 364 |
+
{/* Progress Bar */}
|
| 365 |
+
{progress && (
|
| 366 |
+
<div className="space-y-3">
|
| 367 |
+
<div className="w-full bg-gray-200 rounded-full h-2 overflow-hidden">
|
| 368 |
+
<div
|
| 369 |
+
className="bg-gradient-to-r from-teal-600 to-teal-500 h-2 rounded-full transition-all duration-300"
|
| 370 |
+
style={{ width: `${(progress.current / progress.total) * 100}%` }}
|
| 371 |
+
/>
|
| 372 |
+
</div>
|
| 373 |
+
<p className="text-sm text-gray-600 text-center font-medium">
|
| 374 |
+
{statusMessage || `Processing ${progress.current}/${progress.total} files...`}
|
| 375 |
+
</p>
|
| 376 |
+
</div>
|
| 377 |
+
)}
|
| 378 |
+
|
| 379 |
+
{/* Error Alert */}
|
| 380 |
+
{error && (
|
| 381 |
+
<div className="p-4 bg-red-50 border border-red-200 rounded-lg flex gap-3">
|
| 382 |
+
<svg className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
| 383 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
| 384 |
+
</svg>
|
| 385 |
+
<span className="text-sm text-red-800">{error}</span>
|
| 386 |
+
</div>
|
| 387 |
+
)}
|
| 388 |
+
</div>
|
| 389 |
+
</div>
|
| 390 |
+
</div>
|
| 391 |
+
|
| 392 |
+
{/* Generated Docs Section */}
|
| 393 |
+
{docs && docs.length > 0 && (
|
| 394 |
+
<div className="mb-12">
|
| 395 |
+
<div className="flex items-center justify-between mb-6">
|
| 396 |
+
<h2 className="text-2xl font-bold text-gray-900">Generated Documentation</h2>
|
| 397 |
+
<div className="flex gap-3">
|
| 398 |
+
<button
|
| 399 |
+
onClick={handleGenerateReadme}
|
| 400 |
+
disabled={readmeLoading || !generatedRepoUrl}
|
| 401 |
+
className="flex items-center gap-2 bg-blue-600 hover:bg-blue-700 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition"
|
| 402 |
+
title="Generate project README"
|
| 403 |
+
>
|
| 404 |
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 405 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 406 |
+
</svg>
|
| 407 |
+
{readmeLoading ? 'Generating...' : 'Generate README'}
|
| 408 |
+
</button>
|
| 409 |
+
|
| 410 |
+
<button
|
| 411 |
+
onClick={handleGenerateGitignore}
|
| 412 |
+
disabled={gitignoreLoading || !generatedRepoUrl}
|
| 413 |
+
className="flex items-center gap-2 bg-purple-600 hover:bg-purple-700 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition"
|
| 414 |
+
title="Generate .gitignore"
|
| 415 |
+
>
|
| 416 |
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 417 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 418 |
+
</svg>
|
| 419 |
+
{gitignoreLoading ? 'Generating...' : 'Generate .gitignore'}
|
| 420 |
+
</button>
|
| 421 |
+
|
| 422 |
+
<button
|
| 423 |
+
onClick={downloadAllAsZip}
|
| 424 |
+
disabled={!jsZipLoaded}
|
| 425 |
+
className="flex items-center gap-2 bg-teal-600 hover:bg-teal-700 disabled:bg-gray-400 text-white font-medium py-2 px-4 rounded-lg transition"
|
| 426 |
+
>
|
| 427 |
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 428 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
| 429 |
+
</svg>
|
| 430 |
+
Download All as ZIP
|
| 431 |
+
</button>
|
| 432 |
+
</div>
|
| 433 |
+
</div>
|
| 434 |
+
|
| 435 |
+
<div className="space-y-3">
|
| 436 |
+
{docs.map((file, index) => (
|
| 437 |
+
<div
|
| 438 |
+
key={index}
|
| 439 |
+
className="bg-white border border-gray-200 rounded-lg overflow-hidden hover:shadow-md transition-shadow"
|
| 440 |
+
>
|
| 441 |
+
{/* Accordion Header */}
|
| 442 |
+
<button
|
| 443 |
+
onClick={() => toggleFile(file.filename)}
|
| 444 |
+
className="w-full px-6 py-4 flex items-center justify-between hover:bg-gray-50 transition"
|
| 445 |
+
>
|
| 446 |
+
<div className="flex items-center gap-3 flex-1 text-left">
|
| 447 |
+
<svg
|
| 448 |
+
className={`h-5 w-5 text-teal-600 transition-transform ${expandedFiles[file.filename] ? 'rotate-180' : ''}`}
|
| 449 |
+
fill="currentColor"
|
| 450 |
+
viewBox="0 0 20 20"
|
| 451 |
+
>
|
| 452 |
+
<path fillRule="evenodd" d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" clipRule="evenodd" />
|
| 453 |
+
</svg>
|
| 454 |
+
<span className="font-mono text-sm font-semibold text-gray-900">{file.filename}</span>
|
| 455 |
+
<span className="ml-auto text-xs bg-teal-100 text-teal-800 px-2 py-1 rounded">
|
| 456 |
+
{file.documentation.length} bytes
|
| 457 |
+
</span>
|
| 458 |
+
</div>
|
| 459 |
+
</button>
|
| 460 |
+
|
| 461 |
+
{/* Accordion Content */}
|
| 462 |
+
{expandedFiles[file.filename] && (
|
| 463 |
+
<div className="border-t border-gray-200 bg-gray-50 px-6 py-4">
|
| 464 |
+
{/* Markdown Content */}
|
| 465 |
+
<div className="prose prose-sm max-w-none mb-6 bg-white rounded p-4 max-h-96 overflow-y-auto text-gray-900">
|
| 466 |
+
<div dangerouslySetInnerHTML={{ __html: marked(file.documentation) }} />
|
| 467 |
+
</div>
|
| 468 |
+
|
| 469 |
+
{/* Action Buttons */}
|
| 470 |
+
<div className="flex gap-3 pt-4 border-t border-gray-200">
|
| 471 |
+
<button
|
| 472 |
+
onClick={() => copyToClipboard(file.documentation, file.filename)}
|
| 473 |
+
className="flex-1 flex items-center justify-center gap-2 bg-blue-100 hover:bg-blue-200 text-blue-700 font-medium py-2 px-4 rounded-lg transition"
|
| 474 |
+
>
|
| 475 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 476 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 477 |
+
</svg>
|
| 478 |
+
Copy
|
| 479 |
+
</button>
|
| 480 |
+
<button
|
| 481 |
+
onClick={() => downloadFile(file.documentation, file.filename)}
|
| 482 |
+
className="flex-1 flex items-center justify-center gap-2 bg-green-100 hover:bg-green-200 text-green-700 font-medium py-2 px-4 rounded-lg transition"
|
| 483 |
+
>
|
| 484 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 485 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 16v-4m0 0V8m0 4h4m-4 0H8m7 4v2a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2h14a2 2 0 012 2v6a2 2 0 01-2 2z" />
|
| 486 |
+
</svg>
|
| 487 |
+
Download .md
|
| 488 |
+
</button>
|
| 489 |
+
</div>
|
| 490 |
+
</div>
|
| 491 |
+
)}
|
| 492 |
+
</div>
|
| 493 |
+
))}
|
| 494 |
+
</div>
|
| 495 |
+
</div>
|
| 496 |
+
)}
|
| 497 |
+
|
| 498 |
+
{/* README Section */}
|
| 499 |
+
{readme && (
|
| 500 |
+
<div className="mb-12">
|
| 501 |
+
<div className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 border border-gray-100 overflow-hidden">
|
| 502 |
+
{/* README Header */}
|
| 503 |
+
<div className="bg-gradient-to-r from-blue-600 to-blue-500 px-6 py-4 text-white">
|
| 504 |
+
<div className="flex items-center justify-between">
|
| 505 |
+
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 506 |
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 507 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 508 |
+
</svg>
|
| 509 |
+
README.md
|
| 510 |
+
</h2>
|
| 511 |
+
</div>
|
| 512 |
+
</div>
|
| 513 |
+
|
| 514 |
+
{/* README Content */}
|
| 515 |
+
<div className="p-6">
|
| 516 |
+
<div className="prose prose-sm max-w-none bg-gray-50 rounded-lg p-6 max-h-96 overflow-y-auto">
|
| 517 |
+
<div dangerouslySetInnerHTML={{ __html: marked(readme) }} />
|
| 518 |
+
</div>
|
| 519 |
+
|
| 520 |
+
{/* README Actions */}
|
| 521 |
+
<div className="flex gap-3 mt-6 pt-6 border-t border-gray-200">
|
| 522 |
+
<button
|
| 523 |
+
onClick={() => copyToClipboard(readme, 'README.md')}
|
| 524 |
+
className="flex-1 flex items-center justify-center gap-2 bg-blue-100 hover:bg-blue-200 text-blue-700 font-medium py-2 px-4 rounded-lg transition"
|
| 525 |
+
>
|
| 526 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 527 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 528 |
+
</svg>
|
| 529 |
+
Copy to Clipboard
|
| 530 |
+
</button>
|
| 531 |
+
<button
|
| 532 |
+
onClick={() => downloadFile(readme, 'README')}
|
| 533 |
+
className="flex-1 flex items-center justify-center gap-2 bg-green-100 hover:bg-green-200 text-green-700 font-medium py-2 px-4 rounded-lg transition"
|
| 534 |
+
>
|
| 535 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 536 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
| 537 |
+
</svg>
|
| 538 |
+
Download README.md
|
| 539 |
+
</button>
|
| 540 |
+
</div>
|
| 541 |
+
|
| 542 |
+
{readmeError && (
|
| 543 |
+
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg flex gap-3">
|
| 544 |
+
<svg className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
| 545 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
| 546 |
+
</svg>
|
| 547 |
+
<span className="text-sm text-red-800">{readmeError}</span>
|
| 548 |
+
</div>
|
| 549 |
+
)}
|
| 550 |
+
</div>
|
| 551 |
+
</div>
|
| 552 |
+
</div>
|
| 553 |
+
)}
|
| 554 |
+
|
| 555 |
+
{/* .gitignore Section */}
|
| 556 |
+
{gitignore && (
|
| 557 |
+
<div className="mb-12">
|
| 558 |
+
<div className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 border border-gray-100 overflow-hidden">
|
| 559 |
+
{/* Gitignore Header */}
|
| 560 |
+
<div className="bg-gradient-to-r from-purple-600 to-purple-500 px-6 py-4 text-white">
|
| 561 |
+
<div className="flex items-center justify-between">
|
| 562 |
+
<h2 className="text-2xl font-bold flex items-center gap-2">
|
| 563 |
+
<svg className="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 564 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 565 |
+
</svg>
|
| 566 |
+
.gitignore
|
| 567 |
+
</h2>
|
| 568 |
+
</div>
|
| 569 |
+
</div>
|
| 570 |
+
|
| 571 |
+
{/* Gitignore Content */}
|
| 572 |
+
<div className="p-6">
|
| 573 |
+
<div className="bg-gray-900 text-gray-100 rounded-lg p-6 max-h-96 overflow-y-auto font-mono text-sm whitespace-pre-wrap">
|
| 574 |
+
{gitignore}
|
| 575 |
+
</div>
|
| 576 |
+
|
| 577 |
+
{/* Gitignore Actions */}
|
| 578 |
+
<div className="flex gap-3 mt-6 pt-6 border-t border-gray-200">
|
| 579 |
+
<button
|
| 580 |
+
onClick={() => copyToClipboard(gitignore, '.gitignore')}
|
| 581 |
+
className="flex-1 flex items-center justify-center gap-2 bg-purple-100 hover:bg-purple-200 text-purple-700 font-medium py-2 px-4 rounded-lg transition"
|
| 582 |
+
>
|
| 583 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 584 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 585 |
+
</svg>
|
| 586 |
+
Copy to Clipboard
|
| 587 |
+
</button>
|
| 588 |
+
<button
|
| 589 |
+
onClick={() => downloadFile(gitignore, '.gitignore')}
|
| 590 |
+
className="flex-1 flex items-center justify-center gap-2 bg-green-100 hover:bg-green-200 text-green-700 font-medium py-2 px-4 rounded-lg transition"
|
| 591 |
+
>
|
| 592 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 593 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
|
| 594 |
+
</svg>
|
| 595 |
+
Download .gitignore
|
| 596 |
+
</button>
|
| 597 |
+
</div>
|
| 598 |
+
|
| 599 |
+
{gitignoreError && (
|
| 600 |
+
<div className="mt-4 p-4 bg-red-50 border border-red-200 rounded-lg flex gap-3">
|
| 601 |
+
<svg className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
| 602 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
| 603 |
+
</svg>
|
| 604 |
+
<span className="text-sm text-red-800">{gitignoreError}</span>
|
| 605 |
+
</div>
|
| 606 |
+
)}
|
| 607 |
+
</div>
|
| 608 |
+
</div>
|
| 609 |
+
</div>
|
| 610 |
+
)}
|
| 611 |
+
|
| 612 |
+
{/* Search Section */}
|
| 613 |
+
<div className="mb-12">
|
| 614 |
+
<div className="bg-white rounded-lg shadow-md hover:shadow-lg transition-shadow duration-300 p-8 border border-gray-100">
|
| 615 |
+
<h2 className="text-xl font-semibold text-gray-900 mb-6">Search Documentation</h2>
|
| 616 |
+
|
| 617 |
+
<div className="space-y-4">
|
| 618 |
+
<div>
|
| 619 |
+
<label className="block text-sm font-medium text-gray-700 mb-2">
|
| 620 |
+
Ask a Question
|
| 621 |
+
</label>
|
| 622 |
+
<div className="relative">
|
| 623 |
+
<textarea
|
| 624 |
+
value={searchQuery}
|
| 625 |
+
onChange={(e) => setSearchQuery(e.target.value)}
|
| 626 |
+
onKeyPress={(e) => {
|
| 627 |
+
if (e.key === 'Enter' && e.ctrlKey) {
|
| 628 |
+
handleSearch();
|
| 629 |
+
}
|
| 630 |
+
}}
|
| 631 |
+
placeholder="Ask a question about the code... (Press Ctrl+Enter to search)"
|
| 632 |
+
rows="3"
|
| 633 |
+
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-teal-500 focus:border-transparent outline-none resize-none text-gray-900 placeholder-gray-500"
|
| 634 |
+
/>
|
| 635 |
+
</div>
|
| 636 |
+
</div>
|
| 637 |
+
|
| 638 |
+
<button
|
| 639 |
+
onClick={handleSearch}
|
| 640 |
+
disabled={searchLoading || !generatedRepoUrl}
|
| 641 |
+
className="w-full bg-gradient-to-r from-teal-600 to-teal-500 hover:from-teal-700 hover:to-teal-600 disabled:from-gray-400 disabled:to-gray-400 text-white font-semibold py-3 px-4 rounded-lg transition duration-200 flex items-center justify-center gap-2"
|
| 642 |
+
>
|
| 643 |
+
{searchLoading ? (
|
| 644 |
+
<>
|
| 645 |
+
<svg className="animate-spin h-5 w-5" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
| 646 |
+
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4"></circle>
|
| 647 |
+
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
| 648 |
+
</svg>
|
| 649 |
+
Searching...
|
| 650 |
+
</>
|
| 651 |
+
) : (
|
| 652 |
+
<>
|
| 653 |
+
<svg className="h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 654 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
|
| 655 |
+
</svg>
|
| 656 |
+
Search Docs
|
| 657 |
+
</>
|
| 658 |
+
)}
|
| 659 |
+
</button>
|
| 660 |
+
|
| 661 |
+
{searchError && (
|
| 662 |
+
<div className="p-4 bg-red-50 border border-red-200 rounded-lg flex gap-3">
|
| 663 |
+
<svg className="h-5 w-5 text-red-600 flex-shrink-0 mt-0.5" fill="currentColor" viewBox="0 0 20 20">
|
| 664 |
+
<path fillRule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clipRule="evenodd" />
|
| 665 |
+
</svg>
|
| 666 |
+
<span className="text-sm text-red-800">{searchError}</span>
|
| 667 |
+
</div>
|
| 668 |
+
)}
|
| 669 |
+
</div>
|
| 670 |
+
</div>
|
| 671 |
+
</div>
|
| 672 |
+
|
| 673 |
+
{/* Search Results Section */}
|
| 674 |
+
{searchResults && searchResults.length > 0 && (
|
| 675 |
+
<div className="mb-12">
|
| 676 |
+
<h2 className="text-2xl font-bold text-gray-900 mb-6">
|
| 677 |
+
Search Results
|
| 678 |
+
<span className="ml-3 text-lg font-normal text-gray-500">
|
| 679 |
+
{searchResults.length} result{searchResults.length !== 1 ? 's' : ''}
|
| 680 |
+
</span>
|
| 681 |
+
</h2>
|
| 682 |
+
|
| 683 |
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 684 |
+
{searchResults.map((result, index) => (
|
| 685 |
+
<div
|
| 686 |
+
key={index}
|
| 687 |
+
className="bg-white border border-gray-200 rounded-lg p-6 hover:shadow-md transition-shadow"
|
| 688 |
+
>
|
| 689 |
+
<div className="flex items-start justify-between mb-4">
|
| 690 |
+
<span className="inline-block bg-teal-100 text-teal-800 text-xs font-semibold px-3 py-1 rounded-full">
|
| 691 |
+
{result.filename}
|
| 692 |
+
</span>
|
| 693 |
+
<span className="text-xs text-gray-500">
|
| 694 |
+
Relevance: {(1 / (1 + result.distance)).toFixed(2)}
|
| 695 |
+
</span>
|
| 696 |
+
</div>
|
| 697 |
+
|
| 698 |
+
<div className="prose prose-sm max-w-none text-gray-700 max-h-48 overflow-y-auto">
|
| 699 |
+
<div dangerouslySetInnerHTML={{ __html: marked(result.document) }} />
|
| 700 |
+
</div>
|
| 701 |
+
|
| 702 |
+
<button
|
| 703 |
+
onClick={() => copyToClipboard(result.document, `${result.filename}-result`)}
|
| 704 |
+
className="mt-4 text-sm text-teal-600 hover:text-teal-700 font-medium flex items-center gap-1"
|
| 705 |
+
>
|
| 706 |
+
<svg className="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 707 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
|
| 708 |
+
</svg>
|
| 709 |
+
Copy Result
|
| 710 |
+
</button>
|
| 711 |
+
</div>
|
| 712 |
+
))}
|
| 713 |
+
</div>
|
| 714 |
+
</div>
|
| 715 |
+
)}
|
| 716 |
+
|
| 717 |
+
{/* Empty State */}
|
| 718 |
+
{!docs && !searchResults && !loading && (
|
| 719 |
+
<div className="text-center py-16">
|
| 720 |
+
<svg className="h-16 w-16 text-gray-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
| 721 |
+
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
| 722 |
+
</svg>
|
| 723 |
+
<h3 className="text-lg font-medium text-gray-900 mb-2">No documentation yet</h3>
|
| 724 |
+
<p className="text-gray-600 mb-6">Enter a GitHub repository URL above to get started</p>
|
| 725 |
+
</div>
|
| 726 |
+
)}
|
| 727 |
+
</main>
|
| 728 |
+
|
| 729 |
+
{/* Footer */}
|
| 730 |
+
<footer className="bg-gray-50 border-t border-gray-200 mt-16">
|
| 731 |
+
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8 text-center text-sm text-gray-600">
|
| 732 |
+
<p>DocWizard · AI-powered documentation generator</p>
|
| 733 |
+
</div>
|
| 734 |
+
</footer>
|
| 735 |
+
</div>
|
| 736 |
+
);
|
| 737 |
+
}
|
frontend/src/assets/react.svg
ADDED
|
|
frontend/src/index.css
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
@tailwind base;
|
| 2 |
+
@tailwind components;
|
| 3 |
+
@tailwind utilities;
|
| 4 |
+
|
| 5 |
+
/* Markdown prose styling */
|
| 6 |
+
.prose table {
|
| 7 |
+
border-collapse: collapse;
|
| 8 |
+
width: 100%;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
.prose td, .prose th {
|
| 12 |
+
border: 1px solid #e5e7eb;
|
| 13 |
+
padding: 8px 12px;
|
| 14 |
+
text-align: left;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.prose th {
|
| 18 |
+
background: #f9fafb;
|
| 19 |
+
font-weight: 600;
|
| 20 |
+
color: #111827;
|
| 21 |
+
}
|
| 22 |
+
|
| 23 |
+
.prose code {
|
| 24 |
+
background: #f3f4f6;
|
| 25 |
+
padding: 2px 6px;
|
| 26 |
+
border-radius: 4px;
|
| 27 |
+
font-size: 0.875em;
|
| 28 |
+
}
|
| 29 |
+
|
| 30 |
+
.prose pre {
|
| 31 |
+
background: #1e293b;
|
| 32 |
+
color: #e2e8f0;
|
| 33 |
+
padding: 16px;
|
| 34 |
+
border-radius: 8px;
|
| 35 |
+
overflow-x: auto;
|
| 36 |
+
line-height: 1.5;
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
.prose pre code {
|
| 40 |
+
background: transparent;
|
| 41 |
+
color: inherit;
|
| 42 |
+
padding: 0;
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
.prose h2 {
|
| 46 |
+
font-size: 1.5em;
|
| 47 |
+
font-weight: 700;
|
| 48 |
+
margin-top: 1.5em;
|
| 49 |
+
margin-bottom: 0.5em;
|
| 50 |
+
color: #111827;
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
.prose h3 {
|
| 54 |
+
font-size: 1.25em;
|
| 55 |
+
font-weight: 600;
|
| 56 |
+
margin-top: 1.25em;
|
| 57 |
+
margin-bottom: 0.5em;
|
| 58 |
+
color: #111827;
|
| 59 |
+
}
|
| 60 |
+
|
| 61 |
+
.prose h4 {
|
| 62 |
+
font-size: 1.1em;
|
| 63 |
+
font-weight: 600;
|
| 64 |
+
margin-top: 1em;
|
| 65 |
+
margin-bottom: 0.5em;
|
| 66 |
+
color: #111827;
|
| 67 |
+
}
|
| 68 |
+
|
| 69 |
+
.prose p {
|
| 70 |
+
margin-bottom: 1em;
|
| 71 |
+
line-height: 1.6;
|
| 72 |
+
}
|
| 73 |
+
|
| 74 |
+
.prose ul, .prose ol {
|
| 75 |
+
margin-bottom: 1em;
|
| 76 |
+
padding-left: 1.5em;
|
| 77 |
+
}
|
| 78 |
+
|
| 79 |
+
.prose li {
|
| 80 |
+
margin-bottom: 0.5em;
|
| 81 |
+
}
|
| 82 |
+
|
| 83 |
+
.prose strong, .prose b {
|
| 84 |
+
font-weight: 700;
|
| 85 |
+
color: #111827;
|
| 86 |
+
}
|
| 87 |
+
|
| 88 |
+
.prose em, .prose i {
|
| 89 |
+
font-style: italic;
|
| 90 |
+
color: #374151;
|
| 91 |
+
}
|
| 92 |
+
|
| 93 |
+
.prose a {
|
| 94 |
+
color: #0ea5e9;
|
| 95 |
+
text-decoration: underline;
|
| 96 |
+
}
|
| 97 |
+
|
| 98 |
+
.prose a:hover {
|
| 99 |
+
color: #0284c7;
|
| 100 |
+
}
|
| 101 |
+
|
| 102 |
+
.prose blockquote {
|
| 103 |
+
border-left: 4px solid #e5e7eb;
|
| 104 |
+
padding-left: 1em;
|
| 105 |
+
color: #6b7280;
|
| 106 |
+
font-style: italic;
|
| 107 |
+
margin: 1em 0;
|
| 108 |
+
}
|
| 109 |
+
|
| 110 |
+
.prose hr {
|
| 111 |
+
border-color: #e5e7eb;
|
| 112 |
+
margin: 2em 0;
|
| 113 |
+
}
|
frontend/src/main.jsx
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import React from 'react'
|
| 2 |
+
import ReactDOM from 'react-dom/client'
|
| 3 |
+
import App from './App.jsx'
|
| 4 |
+
import './index.css'
|
| 5 |
+
|
| 6 |
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
| 7 |
+
<React.StrictMode>
|
| 8 |
+
<App />
|
| 9 |
+
</React.StrictMode>,
|
| 10 |
+
)
|
frontend/tailwind.config.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/** @type {import('tailwindcss').Config} */
|
| 2 |
+
export default {
|
| 3 |
+
content: ["./index.html", "./src/**/*.{js,jsx}"],
|
| 4 |
+
theme: {
|
| 5 |
+
extend: {},
|
| 6 |
+
},
|
| 7 |
+
plugins: [require('@tailwindcss/typography')],
|
| 8 |
+
}
|
frontend/vite.config.js
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import { defineConfig } from 'vite'
|
| 2 |
+
import react from '@vitejs/plugin-react'
|
| 3 |
+
|
| 4 |
+
// https://vitejs.dev/config/
|
| 5 |
+
export default defineConfig({
|
| 6 |
+
plugins: [react()],
|
| 7 |
+
})
|
requirements.txt
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
fastapi==0.104.1
|
| 2 |
+
uvicorn==0.24.0
|
| 3 |
+
anthropic==0.7.1
|
| 4 |
+
gitpython==3.1.40
|
| 5 |
+
requests==2.31.0
|
| 6 |
+
python-dotenv==1.0.0
|
| 7 |
+
openai==1.3.0
|
| 8 |
+
faiss-cpu==1.7.4
|
| 9 |
+
sentence-transformers==2.2.2
|
| 10 |
+
httpx==0.27.0
|