Spaces:
Sleeping
Sleeping
| import os | |
| import shutil | |
| import tempfile | |
| from flask import Flask, request, jsonify, render_template_string | |
| import git | |
| import json | |
| import google.generativeai as genai | |
| import traceback | |
| # --- Configure API Key --- | |
| API_KEY = os.environ.get("GOOGLE_API_KEY") | |
| if API_KEY: | |
| try: | |
| genai.configure(api_key=API_KEY) | |
| print("β Google API Key configured successfully.") | |
| except Exception as e: | |
| print(f"π¨ ERROR: Failed to configure Google API Key. Error: {e}") | |
| else: | |
| print("π¨ WARNING: GOOGLE_API_KEY secret not set in Hugging Face Spaces.") | |
| app = Flask(__name__) | |
| HTML_TEMPLATE = """ | |
| <!DOCTYPE html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>AI README Generator π§ </title> | |
| <style> | |
| @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap'); | |
| body { | |
| font-family: 'Inter', sans-serif; | |
| background-color: #f0f2f5; | |
| color: #1c1e21; | |
| margin: 0; | |
| display: flex; | |
| flex-direction: column; | |
| justify-content: center; | |
| align-items: center; | |
| min-height: 100vh; | |
| padding: 20px; | |
| box-sizing: border-box; | |
| } | |
| .container { | |
| background-color: #ffffff; | |
| padding: 40px 50px; | |
| border-radius: 12px; | |
| box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); | |
| width: 100%; | |
| max-width: 700px; | |
| text-align: center; | |
| } | |
| h1 { font-size: 2.2em; color: #1877f2; margin-bottom: 10px; } | |
| p { color: #606770; font-size: 1.1em; margin-bottom: 30px; } | |
| .input-group { display: flex; margin-bottom: 20px; } | |
| #repo-url { flex-grow: 1; padding: 15px; border: 1px solid #dddfe2; border-radius: 6px 0 0 6px; font-size: 1em; outline: none; min-width: 0; } | |
| #repo-url:focus { border-color: #1877f2; box-shadow: 0 0 0 2px rgba(24, 119, 242, 0.2); } | |
| button { padding: 15px 25px; border: none; background-color: #1877f2; color: white; font-size: 1em; font-weight: 600; border-radius: 0 6px 6px 0; cursor: pointer; transition: background-color 0.3s; } | |
| button:hover { background-color: #166fe5; } | |
| .loader { border: 4px solid #f3f3f3; border-top: 4px solid #1877f2; border-radius: 50%; width: 40px; height: 40px; animation: spin 1s linear infinite; margin: 30px auto; display: none; } | |
| @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } | |
| #result-container { display: none; margin-top: 20px; } | |
| #result { margin-top: 10px; padding: 20px; background-color: #f7f7f7; border: 1px solid #dddfe2; border-radius: 6px; text-align: left; white-space: pre-wrap; font-family: 'Courier New', Courier, monospace; max-height: 400px; overflow-y: auto; word-wrap: break-word; } | |
| .action-buttons { text-align: right; margin-top: 15px; } | |
| .action-buttons button { | |
| padding: 10px 20px; | |
| border-radius: 6px; | |
| background-color: #e4e6eb; | |
| color: #1c1e21; | |
| font-weight: 600; | |
| margin-left: 10px; | |
| font-size: 0.9em; | |
| } | |
| .action-buttons button:hover { background-color: #d8dbdf; } | |
| footer { margin-top: 25px; text-align: center; font-size: 0.9em; color: #8a8d91; } | |
| footer a { color: #1877f2; text-decoration: none; font-weight: 600; } | |
| footer a:hover { text-decoration: underline; } | |
| </style> | |
| </head> | |
| <body> | |
| <div class="container"> | |
| <h1>AI README Generator π§ </h1> | |
| <p>Enter a public GitHub repository URL and let a true AI agent analyze the code and generate a README for you.</p> | |
| <form id="repo-form"><div class="input-group"><input type="url" id="repo-url" placeholder="e.g., https://github.com/user/project" required><button type="submit">Generate</button></div></form> | |
| <div class="loader" id="loader"></div> | |
| <div id="result-container"> | |
| <h2>Generated README.md:</h2> | |
| <pre id="result"></pre> | |
| <div class="action-buttons"> | |
| <button id="copy-btn">Copy</button> | |
| <button id="download-btn">Download .md</button> | |
| </div> | |
| </div> | |
| </div> | |
| <footer> | |
| <p>Made with β€οΈ by <a href="https://asadfaizee.is-a.dev/" target="_blank" rel="noopener noreferrer">Asad Faizee</a></p> | |
| </footer> | |
| <script> | |
| const repoForm = document.getElementById('repo-form'); | |
| const loader = document.getElementById('loader'); | |
| const resultContainer = document.getElementById('result-container'); | |
| const resultDiv = document.getElementById('result'); | |
| const copyBtn = document.getElementById('copy-btn'); | |
| const downloadBtn = document.getElementById('download-btn'); | |
| repoForm.addEventListener('submit', async function(event) { | |
| event.preventDefault(); | |
| const url = document.getElementById('repo-url').value; | |
| loader.style.display = 'block'; | |
| resultContainer.style.display = 'none'; | |
| resultDiv.textContent = ''; | |
| try { | |
| const response = await fetch('/generate', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: url }) }); | |
| const data = await response.json(); | |
| if (response.ok) { | |
| resultDiv.textContent = data.readme; | |
| } else { | |
| resultDiv.textContent = 'Error: ' + data.error; | |
| } | |
| resultContainer.style.display = 'block'; | |
| } catch (error) { | |
| resultDiv.textContent = 'An unexpected error occurred: ' + error.toString(); | |
| resultContainer.style.display = 'block'; | |
| } finally { | |
| loader.style.display = 'none'; | |
| } | |
| }); | |
| copyBtn.addEventListener('click', function() { | |
| const resultText = resultDiv.textContent; | |
| const textArea = document.createElement('textarea'); | |
| textArea.value = resultText; | |
| document.body.appendChild(textArea); | |
| textArea.select(); | |
| document.execCommand('copy'); | |
| document.body.removeChild(textArea); | |
| this.textContent = 'Copied!'; | |
| setTimeout(() => { this.textContent = 'Copy'; }, 2000); | |
| }); | |
| downloadBtn.addEventListener('click', function() { | |
| const resultText = resultDiv.textContent; | |
| const blob = new Blob([resultText], { type: 'text/markdown' }); | |
| const url = URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'README.md'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| URL.revokeObjectURL(url); | |
| }); | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| def generate_readme_with_llm(repo_path): | |
| print("Step 4: Starting to analyze repository files.") | |
| file_structure = "" | |
| file_contents = "" | |
| for root, _, files in os.walk(repo_path): | |
| if '.git' in root: | |
| continue | |
| level = root.replace(repo_path, '').count(os.sep) | |
| indent = ' ' * 4 * level | |
| file_structure += f"{indent}{os.path.basename(root)}/\n" | |
| sub_indent = ' ' * 4 * (level + 1) | |
| for f in files[:5]: | |
| file_structure += f"{sub_indent}{f}\n" | |
| try: | |
| with open(os.path.join(root, f), 'r', errors='ignore') as file: | |
| content = file.read(2000) | |
| file_contents += f"\n--- Start of {f} ---\n{content}\n--- End of {f} ---\n" | |
| except Exception: | |
| continue | |
| prompt = f""" | |
| You are an expert technical writer. Analyze the repository context below and generate a professional README.md. | |
| **File Structure:** | |
| ``` | |
| {file_structure} | |
| ``` | |
| **Key File Contents:** | |
| ``` | |
| {file_contents} | |
| ``` | |
| Generate a README.md with these sections: Project Title, About the Project, Getting Started, and Usage. | |
| - Infer purpose, technologies, and setup commands. | |
| - Output must be valid Markdown. | |
| """ | |
| try: | |
| print("Step 5: Sending request to Gemini API...") | |
| # Reverted model name back to user's preference. | |
| model = genai.GenerativeModel('gemini-2.5-flash') | |
| response = model.generate_content(prompt) | |
| print("Step 6: Received response from Gemini API.") | |
| readme_text = response.text.strip().replace("```markdown", "").replace("```", "") | |
| return readme_text | |
| except Exception as e: | |
| print(f"π¨π¨π¨ CRITICAL ERROR during Gemini API call: {e}") | |
| raise | |
| def index(): | |
| return render_template_string(HTML_TEMPLATE) | |
| def generate(): | |
| print("\n--- NEW REQUEST ---") | |
| print("Step 1: Received request to /generate.") | |
| data = request.get_json() | |
| if not data or 'url' not in data: | |
| return jsonify({"error": "Request body must be JSON with a 'url' key."}), 400 | |
| repo_url = data.get('url') | |
| if not repo_url or "github.com" not in repo_url.lower(): | |
| print(f"Validation failed for URL: {repo_url}") | |
| return jsonify({"error": "A valid public GitHub repository URL is required."}), 400 | |
| if not API_KEY: | |
| return jsonify({"error": "Server is missing the GOOGLE_API_KEY. Cannot contact the LLM."}), 500 | |
| temp_dir = tempfile.mkdtemp() | |
| try: | |
| print(f"Step 2: Cloning repository: {repo_url} into {temp_dir}") | |
| git.Repo.clone_from(repo_url, temp_dir) | |
| print("Step 3: Cloning successful.") | |
| readme_content = generate_readme_with_llm(temp_dir) | |
| print("Step 7: Successfully generated README. Sending response.") | |
| return jsonify({"readme": readme_content}) | |
| except Exception as e: | |
| print(f"π¨π¨π¨ AN UNEXPECTED ERROR OCCURRED in /generate route π¨π¨π¨") | |
| traceback.print_exc() | |
| return jsonify({"error": "An unexpected server error occurred. Please check the logs."}), 500 | |
| finally: | |
| print(f"Step 8: Cleaning up temporary directory: {temp_dir}") | |
| shutil.rmtree(temp_dir) | |
| if __name__ == '__main__': | |
| app.run(host='0.0.0.0', port=int(os.environ.get("PORT", 7860))) |