Manisankarrr commited on
Commit
f2f397e
·
0 Parent(s):

project completed

Browse files
.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